diff --git a/.DS_Store b/.DS_Store
index 0317e0dc..5362a3e0 100644
Binary files a/.DS_Store and b/.DS_Store differ
diff --git a/.github/workflows/platformio-ci.yml b/.github/workflows/platformio-ci.yml
index 3149fc63..ca252a0c 100644
--- a/.github/workflows/platformio-ci.yml
+++ b/.github/workflows/platformio-ci.yml
@@ -32,4 +32,4 @@ jobs:
pip install --upgrade platformio
- name: Compile firmware
- run: pio run -e teensy31
+ run: pio run -e pico
diff --git a/ClearUI/ClearUI_Field.cpp b/Archive/ClearUI/ClearUI_Field.cpp
similarity index 100%
rename from ClearUI/ClearUI_Field.cpp
rename to Archive/ClearUI/ClearUI_Field.cpp
diff --git a/ClearUI/ClearUI_Field.h b/Archive/ClearUI/ClearUI_Field.h
similarity index 100%
rename from ClearUI/ClearUI_Field.h
rename to Archive/ClearUI/ClearUI_Field.h
diff --git a/ClearUI/ClearUI_Layout.cpp b/Archive/ClearUI/ClearUI_Layout.cpp
similarity index 100%
rename from ClearUI/ClearUI_Layout.cpp
rename to Archive/ClearUI/ClearUI_Layout.cpp
diff --git a/ClearUI/ClearUI_Layout.h b/Archive/ClearUI/ClearUI_Layout.h
similarity index 100%
rename from ClearUI/ClearUI_Layout.h
rename to Archive/ClearUI/ClearUI_Layout.h
diff --git a/Archive/Docs.md b/Archive/Docs.md
new file mode 100644
index 00000000..8cbf80ff
--- /dev/null
+++ b/Archive/Docs.md
@@ -0,0 +1,1118 @@
+# OMX-27 Documentation
+
+# Table of contents
+1. [Concepts](#concepts)
+ 1. [Layout](#layout)
+ 2. [Encoder](#encoder)
+ 3. [AUX Key](#auxkey)
+ 4. [Potentiometers](#potentiometers)
+ 5. [Key Switches](#keyswitches)
+ 6. [Changing Modes](#changingmodes)
+ 7. [Sub-Modes](#subparagraph1)
+ 8. [Saving Session State](#savingsess)
+2. [Modes](#modes)
+ 1. [MI - Midi Keyboard](#mimode)
+ 2. [DRUM - Drum Keyboard](#drummode)
+ 3. [CH - Chord Keyboard](#chordsmode)
+ 4. [S1 - Sequencer 1](#s1mode)
+ 5. [S2 - Sequencer 2](#s2mode)
+ 6. [GR - Grids Sequencer](#gridsmode)
+ 7. [EL - Euclidean Sequencer](#elmode)
+ 8. [OM - Organelle Mother](#organellemode)
+ 9. [Screensaver](#screensaver)
+3. [Hardware](#hardware)
+
+# Concepts
+
+OMX-27 is a MIDI Keyboard and Sequencer. Both USBMIDI (in/out) and hardware MIDI out (via 1.8" TRS jack) are supported. Various "modes" can be accessed with the encoder and specific functions, parameters or sub-modes can be accessed with the encoder or key-presses/key-combinations.
+
+Sequencer modes have 8 patterns (tracks). Sequencer modes currently send MIDI clock and transport control (start/stop) by default.
+
+CV pitch output is limited to about 4.3 octaves.
+
+USBMIDI should be plug-and-play with any USBMIDI compatible host. iPad works great with the camera connection kit or a [lightning to usb micro cable](https://www.amazon.com/gp/product/B09KLXNYHL). Hardware MIDI TRS output jack is switchable between Type-A and Type-B.
+
+## Layout
+
+
+
+## Encoder
+
+The encoder is the knob directly to the right of the display.
+
+You can use the encoder to modify parameters and change the selected parameter and page.
+
+Short press the encoder once to toggle between selecting parameters and editing them. The selected parameter will be highlighted to reflect which mode you are in.
+
+Long press the encoder to change to a different mode. Once in mode selection, turn the encoder, then short-press to enter the selected mode.
+
+## AUX Key
+
+The top left key is the AUX Key.
+
+In the MIDI Keyboard and Chords modes, holding this key gives you access to shortcuts.
+
+In the sequencer modes, this key is a dedicated start stop button.
+
+This key is also used in many places to quickly edit a parameter. To do this, hold down aux and turn the encoder to quickly edit the selected parameter without needing to press the encoder. If the encoder was already pressed, it is locked in edit mode and the aux shortcut won't work until you exit by pressing the encoder again.
+
+If you are in a submode, the AUX Key can be used to exit out to the main mode.
+
+## Potentiometers
+
+The OMX-27 has 5 potentiometers which are mapped to send continuous controller MIDI messages (CCs). There are 5 banks of CCs available. You can switch banks using the PBNK parameter in the MI and DRUM Modes.
+
+Bank A is selected by default with the CCs set to controller numbers 21, 22, 23, 24 and 7 (volume).
+
+The CC's can also be reconfigured on device in the [CC Config page](#ccconfig), which is found on the last page of the MI Mode parameters. You can also configure the CC banks all at once via the [web configurator](https://okyeron.github.io/OMX-27/webconfig/index.html).
+
+## Key Switches
+
+The functions of the key switches changes depending on which mode you are in.
+
+In MI Mode, the key switches work like a normal MIDI keyboard.
+
+In other modes, the keys have specific functions depending on the mode or submode. Most key interactions should also light up that key's LED.
+
+In this documentation keys will be referred to by number, from left to right and position, top or bottom. Mentions of "White" or "Black" keys are in reference to a standard piano keyboard layout.
+
+### "Black keys" (sharp/flat keyboard keys)
+
+The top row (black) keys are referenced as **Top 1-10**.
+
+The first 2 black keys are Function Keys (FUNC)
+- F1 - First black key
+- F2 - Second black key
+
+FUNC keys are used to perform various shortcuts depending on the mode.
+
+The other black key functions depend on the current mode.
+
+### "White keys" (bottom row)
+
+The bottom row (white) keys are referenced as **Bottom 1-16**.
+
+These have different functions depending on the current mode. For example, in sequencer modes, these are the sequencer step on/off keys.
+
+## Changing Modes
+
+The OMX-27 has multiple modes. Each is independent - only one mode can be used at a time.
+
+Long press the encoder to change modes.
+
+## Sub-Modes
+
+Certain actions will cause you to enter a sub-mode, some examples could be editing MidiFX, configuring the pots, or saving/loading presets. If you are in a sub-mode, the AUX key will typically be flashing or red. A quick press of the AUX key will exit the sub-mode.
+
+## Macro-Modes
+
+Macro modes are like sub-modes in that they are an alternate mode. Macro-modes can take over the keys, display, encoder, and pots.
+
+Macro modes are specialized modes designed to be used to control external gear like the M8, Norns, and Deluge.
+
+You can enter and exit macro modes by double clicking the AUX key in the root level mode.
+
+Learn more about macro modes [here](#mimacromodes)
+
+
+## Saving Session State
+
+To save your current session to memory, first enter Mode Select by holding the encoder. While this is active press AUX to save.
+
+The next time you restart your device, your last active mode will be loaded, and saved patterns and settings will be recalled.
+
+Saving is a long operation and not recommended to do while in the middle of a performance.
+
+---
+
+# Modes
+
+The OMX-27 has multiple modes. Each is independent - only one mode can be used at a time.
+
+Long press the encoder to change modes.
+
+The current modes are:
+- [MI - Midi Keyboard](#mimode)
+- [DRUM - Drum Keyboard](#drummode)
+- [CH - Chords](#chordsmode)
+- [S1](#s1mode) - A step sequencer with 8 patterns that can be up to 64 steps. Only one pattern can be active at a time
+- [S2](#s2mode) - The same as S1 but all 8 patterns are active
+- [GR - Grids Sequencer](#gridsmode) (A rhythm sequencer based on Mutable Instruments Grids)
+- [EL - Euclidean Sequencer](#elmode)
+- [OM - Organelle Mother](#organellemode)
+
+## MI - MIDI
+
+MIDI Keyboard. This mode makes the 26 keys act like a normal musical keyboard on a chromatic scale.
+
+### AUX Key:
+
+The top left key standing by it's lonesome is the AUX Key. While holding down this key, the other 26 keys will no longer play musical notes and instead give you access to quick shortcuts.
+
+Holding the AUX key will let you quickly edit the selected parameter. To do this, hold down aux and turn the encoder to quickly edit the selected parameter without needing to click the encoder. If the encoder was already clicked, it is locked in edit mode and the aux shortcut won't work until you exit by pressing the encoder again.
+
+If you are in a submode like editing midi fx or the arp, the AUX Key can be used to exit out to the main mode.
+
+### KEYS & LEDS
+
+Each of the 26 keys will play a note on the chromatic scale. The keys will light up when pressed. They will also light up to show incoming midi notes.
+
+If a scale is enabled, the keys that are in the scale will be lit up.
+
+### KEYS & LEDS - AUX Button Held
+
+Hold down the AUX key to access quick functions.
+
+#### Top Keys
+- **[1] Previous Parameter** : Selects the previous parameter in the menu
+- **[2] Next Parameter** : Selects the next parameter in the menu
+
+##### MidiFX
+Select which midifx slot to send the keyboard notes to.
+Double click or long hold a MidiFX key to enter the MidiFX sub-mode and edit the effects.
+See [MidiFX](#midifx) for more info.
+
+- **[5] MidiFX Off** : Notes from the drum key will be sent directly out
+- **[6] MidiFX 1**
+- **[7] MidiFX 2**
+- **[8] MidiFX 3**
+- **[9] MidiFX 4**
+- **[10] MidiFX 5**
+
+#### Bottom Keys
+##### The Highest Highs and Lowest Lows
+- **[1] Octave Down**
+- **[2] Octave Up**
+
+##### Arpeggiator
+- **[12] Edit Params** : Enters a pass through arp edit sub-mode allowing you to edit arp values and also play the keyboard. You can also edit the arp in the MidiFX sub-mode but will need to exit the sub-mode to play the keyboard.
+- **[13] Change Pattern** : Cycles through arpeggiator patterns
+- **[14] Change Octave** : Cycles through arpeggiator octave ranges
+- **[15] Toggle Hold** : Toggles the arpeggiator hold function
+- **[16] Power** : Toggles the arpeggiator on and off
+
+### Menu Pages
+
+**Page 1 - Midi Out:**
+- `OCT`: Current Octave
+- `CH`: Active MIDI Channel
+- `VEL`: The velocity level for midi notes
+
+**Page 2 - Inspect:**
+Not editable, this shows which notes and CC's have been sent.
+- `P CC` : Pot CC, this is the CC number of the last pot that was used.
+- `P Val` : This is that last CC value of the last pot that was used.
+- `NOTE` : This is the number of the last note that was sent. Note that notes generated by MidiFX will not be seen here.
+- `VEL` : This is the velocity of the last note that was sent.
+
+**Page 3 - Midi Tools:**
+- `RR`: RoundRobin MIDI Channel distribution
+- `RROF`: RR offset value
+- `PGM`: MIDI program change
+- `BNK`: MIDI bank select.
+
+**Page 4 - Pots & Macros:**
+- `PBNK`: Potentiometer bank select
+- `THRU`: When "On" incoming USBMIDI is passed to TRS MIDI Out.
+- `MCRO`: MIDI Macro Mode Select (default is OFF)
+- `M-CH`: MIDI Macro Mode Channel
+
+**Page 5 - Scales:**
+- `ROOT` : Select the root note for scale mode
+- `SCALE` : Select a scale or turn off scale mode
+- `LOCK` : Locks to the active scale. If this is enabled, you can only play notes in the scale
+- `GROUP` : Groups all the notes of the scale across the lower row of 16 keys.
+
+**Page 6 - Config:**
+- `CC` : Function, press down on the encoder to use. This enters the configuration tool for setting up the CC values for each pot bank.
+
+### Musical Scales
+
+Scales can be turned on using the 5th parameter page. When a scale is enabled, the keys in the scale light up, and the root notes light up a brighter color. You can still play chromatically and out of key.
+
+Enabling the 'LOCK' param makes it so only the notes in the scale send midi notes out.
+
+Turning on the 'GROUP' parameter maps the scale across the lower row of 16 keys. The root note starts on bottom key 2 which would normally be a C in chromatic mode.
+
+These scale settings also control the scale of the Scaler MidiFX when they are set to use the global scale.
+
+### CC Configuration Submode
+
+This mode lets you change the CC values that the potentiometers send out.
+
+Bottom Keys 1-5 let you quickly change the selected bank.
+
+Press the AUX key to exit.
+
+### MidiFX
+
+MidiFX are effects that can be applied after midi data is generated by playing keys or a sequencer and before any midi data goes out of the device.
+
+MidiFX can currently only be used within the: MIDI Keyboard (MI), Chords (CH) and Euclidian (EL) modes.
+
+MidiFX currently only work on internally generated midi, but may support external midi coming in from USB in the future.
+
+MidiFX are arranged in groups of 8 MidiFX. A group of 8 is called a MidiFX Group. In the MI Mode, midi notes generated by playing the keyboard can only be routed to one MidiFX Group. In the sequencer modes, midi notes can be routed to different MidiFX groups by track.
+
+Think of a MidiFX group as a pedalboard for Midi. Midi data comes in from the left and go to MidiFX slot 1, then out of slot 1 to slot 2, until slot 8, then out of the device.
+
+You can change the active MidiFX group in the MI Mode and the Chords modes by holding aux and pressing one of the Top Keys 6-10.
+
+To enter the MidiFX submode, hold AUX, then hold or double click on a MidiFX Key(Top Keys 6-10).
+
+Top Key 1: Copy
+Top Key 2: Paste
+Top Key 1 + 2: Cut
+Top Keys 3 - 10: Select a MidiFX slot
+Bottom Keys: Add or change the type of MidiFX for a slot. You must hold down the MidiFX slot key in order to change.
+
+To move a MidiFX slot around, you can either cut and paste, or you can hold a MidiFX slot key and turn the encoder.
+
+There are several different MidiFX available for each slot. Select a MidiFX slot, and you can edit the parameters for that instance of a MidiFX.
+
+Each MidiFX type has a chance parameter. If this is less than 100%, than there is a chance this effect will not be applied.
+
+Available MidiFX:
+- Chance: Uses randomness to determine if a note passes through: 100% or gets killed: 0%
+- Transpose: Transpose midi notes by semitones or octaves
+- Randomizer: Randomize notes by range, octaves, velocities, and note lengths.
+- Harmonizer: Generate multiple notes from a single note. Great for 1 key chords, or to randomly play chords by setting it's chance parameter to less than 100%
+- Scaler: This forces notes into a specific scale.
+ - By default this uses the global scale. Modifying the root and scale pattern parameters changes the global scale.
+ - You can turn 'GLBL' to off to make this MidiFX instance use it's own unique root and scale pattern.
+- Make Mono: This forces polyphonic midi notes into monophonic.
+- Arpeggiator: This is an advanced arpeggiator. An arpeggiator MidiFX is automatically added to a MidiFX group if you use any of the arpeggiator AUX quick keys.
+ - Try combining the arpeggiator with other MidiFX either before or after the arpeggiator for interesting effects.
+ - Arpeggiators are reasonably resource intensive, try to avoid using more than 3 of them in a MidiFX group for optimal performance.
+
+### Arpeggiator
+
+The arpeggiator is an advanced arpeggiator with many different combinations of patterns, Mod Patterns, and Transpose patterns.
+
+The arpeggiator is actually a MidiFX and is only available in the modes that support MidiFX: MIDI Keyboard (MI), Chords (CH), and Euclidian Sequencer (EL).
+
+In MIDI Keyboard and Chords modes, holding AUX allows you to quickly change basic settings of the arpeggiator. In order to access the more advanced functionalities, you need to access the arpeggiator parameters by either entering the MidiFX Group submode or by accessing the Arpeggiator pass-through mode by using AUX + Bottom Key 12.
+
+The arpeggiator sends clock when in use.
+
+#### Arp Menu Pages:
+
+**Page 1 - Arp Settings 1:**
+- `MODE`: Arpeggiator mode: On, 1-Shot, Once, Hold
+- `PAT`: The pattern of the arpeggiator
+- `RSET`: Determines what will cause the arpeggiator to reset
+- `CHC%`: How likely an incoming note will be used with the arpeggiator. Try playing with in the euclidean sequencer mode
+
+**Page 2 - Arp Settings 2:**
+- `RATE`: How fast the arpeggiator plays
+- `RANG`: How many octaves to advance through
+- `GATE`: How long a note coming out of the arpeggiator is
+- `BPM`: Controls the master tempo
+
+**Page 3 - Arp Settings 3:**
+- `ODIST`: How many semitones to consider an octave. Set to -12 to make the arpeggiator go downwards. Set to a semitone offset if you like to get weird
+
+**Page 4 - Inspect:**
+These are not modifyable
+- `VEL` : Velocity based on first notes that turn on the arp
+- `CHAN` : First note that starts the arp sets the channel
+- `MIDI` : If midi data is output
+- `CV` : If CV data is output
+
+**Page 5 - Mod Pattern:**
+- Each of the 16 steps can be used to modify the arp pattern in various ways
+
+**Page 6 - Transpose Pattern:**
+- Each of the 16 steps can be used to transpose notes in semitones
+- Recommend trying to use for some cool one key bass patterns
+
+
+### MIDI Macro Modes
+
+Midi macro modes are specialized Midi controller modes designed to be used with specific hardware that can be controlled via Midi.
+
+If a midi macro is selected, you can double click the AUX key to enter the midi macro mode, and double click the AUX key to exit the macro mode.
+
+Midi macro modes send control midi commands on the Midi Macro channel, which is `M-CH` in the parameters.
+
+Macro modes are supported in the MI and DRUM modes.
+
+
+#### M8 Macro Mode
+
+From MI Mode, be sure `M8` is selected from the `MCRO` parameter and then double click the AUX button to enter Macro Mode.
+
+`M-CH` should be set to the same value as the Control Map Channel in the M8 MIDI settings screen. Default is set to channel 10.
+
+Double click the AUX button again to exit Macro Mode.
+
+The M8 macro mode has two pages, which change what the Keys do.
+
+* Mute Solo Page:
+
+The bottom row of keys correspond to mutes (orange) and solos (red). The top "black keys" are as follows:
+
+```
+Orange - release all mutes
+Lime - go to mixer screen
+Cyan - snapshot load/paste *
+Magenta - snapshot save/enter selection mode *
+Red - release all solos
+Yellow - waveform display
+Blue - play
+```
+
+
+When M8 is selected from the `MCRO` parameter - potentiometers send on the `M-CH` MIDI channel in both regular keyboard mode and in the macro mode. However, notes played on keys send on the currently selected `CH` MIDI channel.
+
+**Notes:**
+* M8 must be on the Mixer view for snapshots.
+* Snapshot Load uses the M8 key combo [SHIFT]+[OPTION]. On any view with a grid (song, chain, phrase, table, etc.) this key enters selection mode.
+* Snapshot Save uses the M8 key combo [SHIFT]+[EDIT]. On any view with a grid this key pastes the copied contents from selection mode.
+
+* Control Page:
+
+This page lets you navigate the M8 using the keys on the device instead of the keys on the M8.
+
+Top Key 1 and Bottoms Keys 1-3: These correspond to the directional arrow keys.
+
+Top Key 4: Option Key
+Top Key 5: Edit Key
+Bottom Key 6: Shift Key
+Bottom Key 7: Play Key
+
+The right half of the Keyboard is a 1-octave midi keyboard that sends notes on the same midi channel as when not in macro mode.
+
+#### Norns Macro Mode
+
+The Norns macro mode gives you the ability to control the Monome Norns using the OMX-27. In this mode, you can control the three buttons and three encoders using the OMX-27. This is useful if your norns is not located close by and you would like to control it.
+
+Find the `MCRO` parameter in the menu and select `NRN` to enable the Norns Macro mode.
+
+Double click the AUX button to enter Macro Mode. Double click the AUX button again to exit Macro Mode.
+
+##### Norns Setup
+In order for this to work, you will need to setup the [qremote mod](#https://llllllll.co/t/qremote/57549) on your norns.
+
+Obtain the mod here: https://llllllll.co/t/qremote/57549
+
+or install in Maiden
+`;install https://github.com/Quixotic7/qremote`
+
+Enable the mod in the mod menu of norns, connect your OMX-27 to the norns and restart. The easiest way to connect the OMX-27 is to simply use a USB port on the norns.
+
+The qremote mod will default to using midi channel 10. If you are using defaults, make sure your OMX-27 Macro Channel `M-Chan` is set to 10.
+
+Use the default configuration for encoders and buttons:
+
+The default cc’s for the encoders are 58, 62, & 63
+
+The default cc’s for the buttons are 85, 87, & 88
+
+You can change these in the parameter menu. Or edit the script to change the defaults.
+
+
+##### Norns Buttons
+These act the same as if pressing the buttons on the Norns.
+- **[Top 3] B1**
+- **[Bot 4] B2**
+- **[Bot 5] B3**
+
+##### Norns Encoders
+Use the OMX-27 Encoder to control one of the Norn's encoders. Since the OMX-27 only has one encoder, shortcut keys are used to determine which encoder will be controlled.
+- **[Top 5] Enc 1**
+- **[Bot 6] Enc 2**
+- **[Bot 7] Enc 3**
+
+##### Norns Navigation
+The keys of the OMX-27 are used to emulate up/down left/right keys useful for menu navigation. These work by controlling encoder 1 or encoder 2, sending one tick either clockwise or counter-clockwise.
+- **[Top 1] Up**
+- **[Bot 1] Left**
+- **[Bot 2] Down**
+- **[Bot 3] Right**
+
+
+#### Deluge Macro Mode
+
+The latest Deluge Community Firmware has added a new feature called Midi-Follow.
+
+https://github.com/SynthstromAudible/DelugeFirmware/blob/community/docs/features/midi_follow_mode.md
+
+This mode provides default CC mappings to control many of the synth parameters with a midi controller and to be able to play the active instrument without needing to midi learn everything.
+
+This Macro Mode provides parameter banks to control every available parameter on the Deluge using the 5 pots of the OMX-27. The Deluge will also send back the values of the parameters to the OMX-27 which will update the values in each bank on the OMX-27. This will only work if you are connected via USB as the OMX-27 does not have TRS Midi-in. Values are updated when changing them from the gold knobs, or from edit view, when entering a clip view, or when changing a synth or kit.
+
+##### Deluge Setup
+
+Download and install the latest Deluge Nightly Firmware: https://github.com/SynthstromAudible/DelugeFirmware/releases . This Midi-Follow feature is not included in the 1.0.1 release. There is also a new beta build released which should have the feature, but it is not confirmed.
+
+To install the FW, place the `.bin` file on your sd card, ensuring there are no other `.bin` files. Carefully reinsert the SD Card in the Deluge, if it is misangled it can fall into the device. Turn on the Deluge while holding the shift key.
+
+Hold Shift + Click the main Select Knob, find Midi, click the Select Knob, find "MIDI-FOLLOW", click the select Knob, find "CHANNEL", click the select Knob, set each channel to something, I recommend channel 10. Also from the "MIDI-FOLLOW" level, select "FEEDBACK" and set the channel to 10 as well.
+
+For further help see https://github.com/SynthstromAudible/DelugeFirmware/blob/community/docs/features/midi_follow_mode.md
+
+##### OMX Setup
+
+Find the `MCRO` parameter in the menu and select `DEL` to enable the Deluge Macro mode. Change Macro Channel `M-CH` to 10 or whatever you set your Deluge to.
+
+Double click the AUX button to enter Macro Mode. Double click the AUX button again to exit Macro Mode.
+
+##### KEYS & LEDS
+The keyboard is split into two halves.
+
+The right octave of the keyboard works the same as the midi keyboard from MI Mode and will inherit it's settings.
+
+The left half of the keyboard is dedicated to selecting different banks of parameters.
+
+Some keys will have multiple banks assigned to them, to access the other banks, click the key a second time.
+
+- **[Top 1] Env 1**
+- **[Top 2] Env 2**
+- **[Top 3] LPF**
+- **[Top 4] HPF**
+- **[Top 5] EQ**
+- **[Bot 1] Master**
+- **[Bot 2] OSC 1**
+- **[Bot 2] FM 1** : Click the key a second time
+- **[Bot 3] OSC 2**
+- **[Bot 3] FM 2** : Click the key a second time
+- **[Bot 4] LFO Delay Reverb**
+- **[Bot 4] ModFX** : Click the key a second time
+- **[Bot 5] Distortion Noise**
+- **[Bot 6] Arp Sidechain**
+- **[Bot 7] Custom 1** : Bank of 5 parameters you can midi learn to whatever
+- **[Bot 7] Custom 2** : Click the key a second time
+
+##### KEYS & LEDS - AUX Button Held
+
+###### Select a parameter
+This will select a parameter. If you click the encoder to enter edit mode the encoder can be used to change the value without needing to pickup the pot.
+- **[Top 1] Param 1**
+- **[Top 2] Param 2**
+- **[Top 3] Param 3**
+- **[Top 4] Param 4**
+- **[Top 5] Param 5**
+
+
+###### Change octave
+This effects the keyboard on the right half.
+- **[Bot 1] Octave Down**
+- **[Bot 2] Octave Up**
+
+###### Lock the AUX View
+This locks the AUX view, allowing you to use the AUX shortcuts without holding down the AUX key. To unlock, use the AUX button again or press the lock AUX key.
+- **[Bot 4] Lock AUX**
+
+###### Revert Values
+This will revert the values of the current bank to their previous state. The previous state is saved whenever the bank is changed, or when the values are updated from incoming Midi from the Deluge. This is fun to play with with the effects, crank up the bitcrush, delay, or reverb temporally, then press this button to revert it back to what it previously was.
+- **[Bot 8] Revert Bank**
+
+
+##### POT Pickups
+Depending on which bank is selected, the OMX-27 will send out different CC's for each of the 5 potentiometers. When a change is detected on a pot, the screen will update to show the name of the current bank, parameter, and value that is being sent.
+
+In the MI and Drum modes, the pot values that are sent jump to the current position of the pot. In the Deluge Macro, you will need to pickup the value before it is sent. The small triangle on the screen represents the current raw value of the pot, the horizontal bar represents the value for that parameter. You will need to turn the knob left or right to pickup the value.
+
+The Deluge can also be setup to pickup, however this macro will work best if the Deluge is set to jump.
+
+##### Encoder
+The encoder can be used to send out values without needing it to be picked up. To edit a parameter, click the encoder, then turn. There is not currently a visual representation to provide feedback for this. If you click the encoder again, you can scroll to change which parameter in the bank to control. You can also use the AUX shortcuts to quickly jump to parameters, or lightly wiggle a knob.
+
+---
+
+## DRUM - Drum Keyboard
+
+This mode shares a lot in common with the MI mode. The main difference is that instead of each key being a key on a chromatic keyboard, each of the 26 keyboard keys can be assigned to send out a unique note, velocity, and midi channel. This is useful if you want to use the OMX to play an external drum machine or samples, or multiple drum machines on different midi channels, or for any purpose you'd like really, feel free to get creative!
+
+The grouping of 26 keys is called a "Drum Kit" and you can store up to 8 different drum kits on the OMX.
+
+Press any key to play it. A note on will be sent when pushed and a note off will be sent once released.
+
+**Selected Drum Key** - This is the last key that was pressed down, it is visually represented by a flashing LED.
+
+The first two pages on the OMX display can be used to change the settings for the selected drum key. Each key can be configured to send a unique note number, velocity, midi channel, and be routed to one of the 5 MidiFX slots. Yes, you can use multiple MidiFX at once. You could have one drum key be routed to MidiFX 1 which has an arp to play a bassline, another key could go to MidiFX 2 with a different arp to play a melody, then another key could be setup to play a kick drum which is routed to MidiFX 3 which has a randomizer enabled to randomly vary the velocity each time it is played. Go wild, get creative, there are no bounds, welcome to OMX!
+
+### Menu Pages
+**Page 1 - DrumKey 1:**
+Values apply to the selected drum key
+- `NOTE` : Midi note number that the drum key will send
+- `CH` : Midi Channel that the drum key will send
+- `VEL` : Velocity of the note the drum key sends
+- `FX#` : Number of the MidiFX slot that the note is sent to
+
+**Page 2 - DrumKey 2:**
+- `HUE` : Changes the color of the selected drum key
+- `HUE RND` : Function, press down on the encoder to use. This will randomize all the hues in the current drum kit.
+
+**Page 3 - Scales:**
+This changes the current scale settings. While a scale won't normally apply to the drum kit, the scale settings are shared globally with the MidiFX. So if you are using a scale MidiFX, this makes it easy to change up the scale without needing to edit the MidiFX.
+- `ROOT` : Root note of the scale.
+- `SCALE` : Which scale to use.
+- `LOCK` : Only notes in the scale can be played, has no effect on the drum kit keys.
+- `GROUP` : Groups the notes in keyboard view accross the lower keys, no effect on the drum kit keys.
+
+**Page 4 - Inspect:**
+Not editable, this shows which notes and CC's have been sent.
+- `P CC` : Pot CC, this is the CC number of the last pot that was used.
+- `P Val` : This is that last CC value of the last pot that was used.
+- `NOTE` : This is the number of the last note that was sent. Note that notes generated by MidiFX will not be seen here.
+- `VEL` : This is the velocity of the last note that was sent.
+
+**Page 5 - Pots & Macros:**
+Settings for the potbank, midi, and macro modes.
+- `PBNK` : Pot Bank - Determines which potbank is active.
+- `THRU` : Midi Thru - If this is on, incoming midi from USB will be sent out the TRS midi jack.
+- `MCRO` : Macro - Determines which macro is active. Macros can be entered by double clicking the AUX key.
+- `M-CH` : Macro channel - Determines which midi channel is used by the macro.
+
+**Page 6 - Config**
+- `CC CFG` : Function, press down on the encoder to use. This enters the configuration tool for setting up the CC values for each pot bank.
+
+### KEYS & LEDS - Main Screen
+Each key represents a drum pad in your drum kit.
+
+Pressing a key will send a note on. Releasing it will send a note off. Which note is sent for each key can be configured by pressing the key, then editing it's varaibles in page 1 and 2 of the menu.
+
+The flashing key is the Selected Drum Key.
+
+Each key can be any color determine by the HUE variable of each drum key.
+
+### KEYS & LEDS - AUX Button Held
+Hold down the AUX key to access quick functions.
+
+#### Top Keys
+- **[1] Previous Parameter** : Selects the previous parameter in the menu
+- **[2] Next Parameter** : Selects the next parameter in the menu
+
+##### saving and loading drum kits
+- **[3] Load Kit** : Use this to load a kit
+- **[4] Save Kit** : Use this to save a kit
+
+##### MidiFX
+Unlike the MI Keyboard mode, the midifx slot only applys to the selected drum key.
+See MidiFX for more info.
+- **[5] MidiFX Off** : Notes from the drum key will be sent directly out
+- **[6] MidiFX 1**
+- **[7] MidiFX 2**
+- **[8] MidiFX 3**
+- **[9] MidiFX 4**
+- **[10] MidiFX 5**
+
+#### Bottom Keys
+##### Quickly switch kits
+- **[1] Load Prev Kit** : Loads the next kit out of 8, any changes to the current kit will be autosaved
+- **[2] Load Next Kit** : Loads the previous kit out of 8, any changes to the current kit will be autosaved
+
+##### Arpeggiator
+- **[12] Edit Params** : Enters a pass through arp edit sub-mode allowing you to edit arp values and also play the keyboard. You can also edit the arp in the MidiFX sub-mode but will need to exit the sub-mode to play the keyboard.
+- **[13] Change Pattern** : Cycles through arpeggiator patterns
+- **[14] Change Octave** : Cycles through arpeggiator octave ranges
+- **[15] Toggle Hold** : Toggles the arpeggiator hold function
+- **[16] Power** : Toggles the arpeggiator on and off
+
+### Potentiometers
+
+
+
+---
+
+## CH - Chords
+
+Ever wanted to play insanely complex chords with the click of a button? Well now you can! In Chord mode, the bottom 16 keys can each be assigned to play a unique chord.
+
+### UI Views
+
+There are two UI views: "Split" and "Full". This can be changed on page 2. By default, the UI layout is in "Split Mode" meaning the right half of the keyboard works like a 1 octave keyboard and the left half will give you 8 chords that can be played. In "Full" mode, each of the 16 bottom keys will play chords.
+
+### Key Modes
+
+There are several different modes available which can be switched using the top keys 3, 4, & 5.
+
+- **[Top 3] Play Mode**
+- **[Top 4] Edit Mode**
+- **[Top 5] Strum Mode**
+
+#### Play Mode
+- **[Top 3] Play Mode**
+
+This mode is where you want to be if you would like to play chords and the keyboard(Split UI Mode) at the same time. Switching to this mode will bring the menu to the first page, displaying a keyboard that shows the notes of the last chord key that was pressed. You can still edit chords through the menu by switching pages.
+
+#### Edit Mode
+- **[Top 4] Edit Mode**
+
+This mode is for editing the available chords and will bring the menu to the chord edit page.
+
+When in the edit mode and in the "Split" UI view, you can hold down a chord key on the left half and press a key on the right half to set the root note for basic chords. For interval chords, the right half will not change anything.
+
+The first two top keys, key 1 and key 2 act as function keys F1 and F2 in this mode.
+
+- **[Top 1 - F1] Edit Chord** : Holding F1 and pressing a chord key will enter a edit chord submode.
+- **[Top 2 - F2] Copy Chord** : Holding F2 and pressing a chord key will save the selected chord to the newly selected chord slot.
+
+#### Strum Mode
+- **[Top 5] Strum Mode**
+
+This mode allows you to strum chords using the encoder. The UI view will change to "Full" in this mode.
+
+To use this mode hold down a chord key and turn the encoder CW or CCW. Only the last pressed chord will be strummed. Multiple chords will not be strummed.
+
+##### Strum Pot Parameters.
+In strum mode, the 5 pots are used to change the behaviour of the strum.
+
+- **[Pot 1] Sens - Sensitivity** : This determines how much the encoder needs to be turned to trigger a new note
+- **[Pot 2] Wrap** : If this is off, the chord can be strummed once, if this is on, the chord will wrap back to the beginning like an arpeggio.
+- **[Pot 3] Increment / Octave** : This is only valid if Wrap is on. If it is, each time the chord wraps the notes will increase by an octave. This value determines how many octaves will be added before resetting.
+- **[Pot 4] Sustain** : This value determines how long each strum note will be played for.
+- **[Pot 5] Not Assigned**
+
+### Chord Key Settings
+
+A chord key is either bottom key 1-8 in "Split" UI mode or bottom key 1-16 in "Full" UI Mode. Pushing a chord key will play a chord, and releasing the key will stop playing the chord. Multiple Chord Keys can be pressed at once, and also combined with the 1 Octave midi keyboard on the right half in "Split" UI mode.
+
+Each chord key can have a unique chord type, velocity, midi channel, and be routed to one of 5 MidiFX(#midifx) slots.
+
+The last chord key that was pressed becomes the selected chord key. This is visually represented on the LEDs as that key will stay lit up
+
+**Page 4 - Chord Key Settings:**
+These parameters apply to the selected chord key
+- `TYPE` : Determines the chord type: Basic`BASC` or Interval`INTV`, see section below on chord types.
+- `MIFX` : Which [MidiFX](#midifx) will this chord be sent to?
+- `VEL` : Velocity of the notes in the chord
+- `MCHAN` : Midi channel of the notes that this chord gets sent to
+
+### Chord Types
+
+Two types of chords are currently available: Basic and Interval. Basic chords don't have many settings and are quick to tweak. Interval chords have a lot more options and are linked to the current global musical scale.
+
+#### Basic Chords
+These chords have no relation to the current global musical scale.
+
+All the parameters are shown on a single page that will show 4 ghosts.
+
+- `NOTE` : Determines the root note of the chord
+- `OCTAVE NUMBER` : Determines the octave of the root note
+- `GHOSTS` : The ghosts determine how the chord is voiced. Each ghost represents the order of the notes in the chord. The vertical position of a ghost determines the velocity of that notes. A large white ghost will play a note in the same octave as the root note. A large black ghost will play a note one octave below. A short white ghost will play a note one octave above.
+- `SCALE` : Determines the scale of the chord. The last scale is called "Custom" and will let you manually set the notes in the chord.
+
+##### Custom Chords
+If `SCALE` is set to `Custom` an additional page in the menu will be revealed. In this page you can program up to 6 notes.
+
+The first 4 notes will be modified +- an octave, or turned off based on your `GHOST` settings
+
+The value of each note in a custom chord is defined as a semitone from the root note of the chord. For a C Maj basic triad chord, you would set this to `RT +4 +7` . `RT` means root note.
+
+#### Interval Chords
+These chords are linked to the current global musical scale. If you play an interval chord and it does not sound good, start by seeing if you have a global scale enabled, and make sure it's not chromatic.
+
+##### Interval Menu Page 1
+- `#NTS` - Number of notes : How many notes to play, 1 - 4
+- `DEG` - Degree : Determines which degree of the the current global scale to start the chord on. If the global scale was C Maj, then Deg 0 would play a chord that starts on C, Deg 1 would play a chord that starts on D, Deg 6 would play a chord that starts on B.
+- `OCT` - Octave : The octave of the chord is determined by the global octave +- this value.
+- `TPS` - Transpose : This will transpose the chord by a seminote. Do note that if you transpose a interval chord it will no longer be in scale.
+
+##### Interval Menu Page 2
+- `SPRD` - Spread : This determines how many octaves the chord is spread out across the keyboard.
+- `ROT` - Rotate : This rotates the notes of the chord. for a C Maj Triad, rot of 0 will play C E G, rot of 1 will make E the lowest note, playing E G C+1oct, rot of 2 will make G the lowest note
+- `VOIC` - Voicing : Changes the voicing of the chord. Still stays in scale, but will shift notes or add additional notes.
+
+##### Interval Menu Page 3
+- `UPDN` - Spread Up & Down : This will spread the notes out in a negative octave and positive octave.
+- `QRTV` - Quartal Harmony : This enables Quartal Harmony. I have no idea what this is doing music theory wise, but it sounds cool. Technically it's bumping the first note up two octaves, the third note up one octave, and the fourth note down 1 octave. It's supposed to separate the notes by 4ths.
+
+### Menu Pages
+
+##### Menu Page 1 - Keyboard
+This page will display a keyboard on the screen showing which notes are being played from the last chord key that was pressed.
+
+##### Menu Page 2 - Chord Mode Settings
+- `UI` : Change the UI View from `SPLIT` or `FULL`. Split view adds a 1-octave midi keyboard on the right half of the keys. in Full view, all 16 of the bottom keys play chords.
+
+##### Menu Page 3 - Keyboard Midi Settings
+These settings apply to the 1-octave keyboard on the right side if the UI View is in `SPLIT` view. Each chord has unique settings.
+- `OCT`: Current Octave. This value also changes the base octave used by interval chords
+- `CH`: Active MIDI Channel
+- `VEL`: The velocity level for midi notes
+
+##### Menu Page 4 - Pots and Macros
+- `PBNK`: Potentiometer bank select
+- `THRU`: When "On" incoming USBMIDI is passed to TRS MIDI Out.
+- `MCRO`: MIDI Macro Mode Select (default is OFF)
+- `M-CH`: MIDI Macro Mode Channel
+
+##### Menu Page 5 - Scale Settings
+The scale settings apply to the 1-octave keyboard on the right side if the UI View is in `SPLIT` view and also will effect the interval chord keys.
+A scale should be set to something other than chromatic to get good results from interval chords.
+- `ROOT` : Select the root note for scale mode. This changes what note the interval chords will play.
+- `SCALE` : Select a scale or turn off scale mode
+- `LOCK` : Locks to the active scale. If this is enabled, you can only play notes in the scale
+- `GROUP` : Groups all the notes of the scale across the lower row of 16 keys.
+
+##### Menu Page 6 - Chord Key Settings
+This changes the settings of the selected chord key. See [Chord Key Settings](#chordkeysettings)
+
+##### Menu Page 7 - Basic or Interval Chord Key Settings
+These pages will be different depending if the selected chord key is set to Basic or Interval.
+See [Basic Chords](#basicchords) or [Interval Chords](#intervalchords)
+
+### KEYS & LEDS - AUX Button Held
+Hold down the AUX key to access quick functions.
+
+#### Top Keys
+- **[1] Previous Parameter** : Selects the previous parameter in the menu
+- **[2] Next Parameter** : Selects the next parameter in the menu
+
+#### Saving and loading
+- **[3] Load Bank**
+- **[4] Save Bank**
+
+##### MidiFX
+This sets the MidiFX slot that either the keyboard in split UI view is being sent to, or the selected chord key is being sent to. Whichever key was last used determines this. You can also change the midifx slot a chord key is sent to from the menu, see [Chord Key Settings](#chordkeysettings)
+Hold or double click a MidiFX key to enter the MidiFX submode.
+See [MidiFX](#midifx) for more info.
+- **[5] MidiFX Off**
+- **[6] MidiFX 1**
+- **[7] MidiFX 2**
+- **[8] MidiFX 3**
+- **[9] MidiFX 4**
+- **[10] MidiFX 5**
+
+#### Bottom Keys
+##### Change Octave
+This changes the global octave. This value will change the octave of the midi keyboard in split view and also change the base octave of interval chords.
+- **[1] Prev Octave**
+- **[2] Next Octave**
+
+##### Arpeggiator
+This effects the Arpeggiator on the currently selected MidiFX slot that the midi keyboard is being sent to.
+- **[12] Edit Params** : Enters a pass through arp edit sub-mode allowing you to edit arp values and also play the keyboard. You can also edit the arp in the MidiFX sub-mode but will need to exit the sub-mode to play the keyboard.
+- **[13] Change Pattern** : Cycles through arpeggiator patterns
+- **[14] Change Octave** : Cycles through arpeggiator octave ranges
+- **[15] Toggle Hold** : Toggles the arpeggiator hold function
+- **[16] Power** : Toggles the arpeggiator on and off
+
+### Saving and Loading
+- **[AUX + Top 3] Load Bank**
+- **[AUX + Top 4] Save Bank**
+
+Use these shortcuts to save and load banks of chords. There are 8 available banks. If you load a bank other than the current one, the current bank will be autosaved. You can revert changes to your current bank by loading the same bank again.
+
+---
+
+## S1 - Sequencer 1
+
+Step sequencer - One pattern active at a time.
+
+Layout:
+
+### "Black keys" (sharp/flat keyboard keys)
+
+The first 2 black keys are Function Keys (FUNC)
+- F1 - First black key
+- F2 - Second black key
+
+The next 8 are Pattern Keys and they select the active sequence pattern (P1-P8).
+
+Hold a key (long press) to access parameters for that pattern. This is "Pattern Params".
+
+
+### "White keys" (bottom row)
+
+Sequencer Step Keys - These are your sequencer step on/off keys.
+
+Hold a key (long press) to access parameters for that step. This is "Note Select / Step Parameters". F1 + Step Key is also a quick shortcut.
+
+Keys/Commands:
+ - AUX is Start/Stop
+ - Start/Stop sends MIDI transport control, and MIDI clock when running
+ - Pattern Key: Selects playing pattern
+ - F1 + AUX: Reset sequences to first/last step
+ - F2 + AUX: Reverse pattern direction
+ - F1 + Pattern Key: Enter __Step Record__ (transport must be stopped)
+ - F2 + Pattern Key: Mute that pattern
+ - F1 + Step Key: Enter __Note Select / Step Parameters__
+ - Long press a Step Key: Enter __Note Select / Step Parameters__
+ - Long press a Pattern Key: Enter __Pattern Parameters__
+ - AUX-key exits sub-modes
+ - Hold F1 + F2: first 4 "white keys" select "page" of the current pattern (depending on pattern length)
+
+Parameters:
+(see below)
+
+
+### Note Select / Step Parameters
+
+Long press a step key to enter this mode. Here you can change the note values (note number, velocity, note length and octave), set CC parameter-lock values with the knobs, and set step parameters (step events, step probability, trig conditions).
+
+While in Note Select, the rightmost and leftmost keys will blink (orange or blue)- these 2 keys will shift the current octave up or down.
+
+Press AUX to exit Note Select.
+
+Parameters:
+
+Page 1:
+- `NOTE`: midi note number
+- `OCT`: octave
+- `VEL`: note velocity
+- `LEN`: note length in steps (1-16)
+
+Page 2:
+- `TYPE`: step event type (see below)
+- `PROB`: percentage of the step triggering
+- `COND`: trig conditions (see below)
+
+Page 3: (set CC parameter-locks)
+- `L-1`: pot 1 p-lock value for this step
+- `L-2`: pot 2 p-lock value for this step
+- `L-3`: pot 3 p-lock value for this step
+- `L-4`: pot 4 p-lock value for this step
+
+Touching any potentiometer while in Note Select will set that p-lock value.
+
+To reset/erase a p-lock - Highlight the parameter and turn the encoder to the left.
+
+#### Step Events (TYPE):
+"-" mute
+"+" play
+"1" reset to first step
+">>" set parttern direction forward
+"<<" set parttern direction reverse
+"#?" jump to random step number
+"?" set random event (of any of the previous events) for that one step
+
+#### Trig conditions - A/B Ratios (COND):__
+Play that step on the A cycle of B total cycles (or bars) of the pattern. Default is 1:1 (every time).
+First number - play step on that cycle thru the pattern
+Second number - resets the counter after that pattern cycle.
+
+So 1:4 would play on the first cycle, not play on the next three and then reset (after the 4th cycle). 3:8 would play only the 3rd cycle and reset after the 8th.
+
+#### Step Record
+
+(SH-101-ish style note entry)
+
+Holding F1 + a Pattern Key will enter Step Record Mode.
+
+Enter notes from the keyboard and the sequence step will automatically advance to the next step. Change knob 1-4 positions to set a CC parameter lock for that step. Knob #5 (far right) will enter a velocity value for that step (there is no visual feedback when entering values from the knobs.
+
+If you want to skip steps while entering notes, use the encoder button to select the STEP parameter and rotate to the step you want to change/update. While a step is selected, you can also record plocks/velocity for that step with the knobs without changing the note value.
+
+There are two pages of parameters in Step Record. First is the current octave (OCT), step number (STEP), note-value (NOTE), and pattern number (PTN). Second shows the step event parameters TYPE, PROB and COND as described above.
+
+Press AUX to exit Step Record.
+
+Keys/Commands:
+- Potentiometers 1-4 set a CC parameter lock
+- Potentiometers 5 sets a step velocity
+- AUX exit this sub-mode
+
+
+### Pattern Parameters
+
+Long press Pattern Key to enter Pattern Params Mode.
+
+Turning the encoder will show different pages of parameters.
+
+A short-press on the encoder will select the active parameter for editing. Press the encoder repeatedly until nothing is selected to change pages.
+
+Press AUX to exit Pattern Parameters.
+
+Parameters:
+
+Page 1:
+- `PTN`: selected pattern
+- `LEN`: pattern length
+- `ROT`: rotation
+- `CH`: midi channel
+
+Page 2 (see Sequence Reset Automation below):
+- `START`: steart
+- `END`: end
+- `FREQ`: frequency
+- `PROB`: probability
+
+Page 3:
+- `RATE`: default note length (1/64th to whole note)
+- `SOLO`: MIDI solo
+
+Keys/Commands:
+- Step Keys set pattern length
+- F1 + pattern copies pattern
+- F2 + pattern pastes pattern (to other pattern slot)
+- F1 + F2 + pattern clears the pattern back to GM drum map default (and clears all plocks)
+
+(you can paste multiple times - paste buffer should stay the same until you copy again)
+
+MIDI solo:
+Set a pattern to MIDI solo and you can play the keyboard while that pattern is selected.
+
+Note - once in MIDI solo, you will only be able to change the active pattern by using the encoder knob.
+
+### Pattern Parameters: Sequence Reset Automation
+
+This is located on the second page of pattern parameters
+
+The goal of this "Sequence Reset Automation" feature was developed in the spirit of classic sequencers that can generate more complex sequences from simpler ones by setting any step in a given sequence to trigger a "reset" based on some constraint (i.e., number of cycles, probability, random).
+
+Note - This behavior is a pattern-based solution. You can also execute step-based resets in Step Parameters.
+
+Settings:
+
+- START (Currently 0 - PatternLength-1): Use this to set the start step in current pattern to reset to for beginning a new cycle.
+
+- END (Currently 0 - PatternLength-1): Use this to set the last step in current sequence to end/reset pattern cycles. This in essence is the step that will be used to trigger resets.
+
+- FREQ of trigger reset (i.e., every X sequence cycle iterations)
+
+- PROB of triggering reset (percentage)
+
+NOTE: Setting STEP = 0 and PROB = 1 dictates random trigger steps which can lead to interesting results by jumping to random position/step.
+
+---
+
+## S2 - Sequencer 2
+
+Step sequencer - All patterns active.
+
+Keys/Commands:
+ - AUX is Start/Stop
+ - Start/Stop sends MIDI transport control, and MIDI clock when running
+ - Pattern Key: Selects active pattern
+ - Encoder changes "page" for sequence parameters (with no parameter highlighted)
+ - Short-press encoder to highlight active parameter to edit
+ - F1 + AUX: Reset sequences to first/last step
+ - F2 + AUX: Reverse pattern direction
+ - F1 + Pattern Key: Enter __Step Record__
+ - F2 + Pattern Key: Mute that pattern
+ - F1 + Step Key: Enter __Note Select / Step Parameters__
+ - Long press a Step Key: Enter __Note Select / Step Parameters__
+ - Long press a Pattern Key: Enter __Pattern Parameters__
+ - AUX-key exits sub-modes
+ - Hold F1 + F2: first 4 "white keys" select "page" of the current pattern (depending on pattern length)
+
+Parameters:
+- `PTN`: selected pattern
+- `TRSP`: transpose (by semitones)
+- `SWNG`: swing
+- `BPM`: tempo
+
+- `SOLO`: set the current pattern to MIDI Solo
+- `LEN`: pattern length
+- `RATE`: default note length (1/64th to whole note)
+- `CV`: enable to send CV from this pattern
+
+
+In the sequencer modes, the default setup is a GM Drum Map with each pattern on a consecutive midi channel. So that's notes 36, 38, 37, 39, 42, 46, 49, 51 on channels 1-8.
+
+---
+
+## GR - Grids Sequencer
+
+An adaptation of the Mutable Instruments "Topographic drum sequencer" module.
+
+See the original [Grids Manual](https://mutable-instruments.net/modules/grids/manual/) [or a video ?] for more.
+
+Grids is a 4-channel/instrument MIDI trigger generator specialized in the creation and sculpting of rhythmic patterns. The "grid" refers to a map or library of preset drum patterns arranged in a 5x5 grid - which you can steer using X/Y controls.
+
+Typical drum use would be Bass Drum, Snare, Closed HiHat, Open HiHat (The default note numbers are mapped to these in the GM drum map).
+
+### Quick Keys
+Grids has many quick keys. Pressing these keys quickly jumps the display to select a specific parameter which can be adjusted with the encoder.
+
+#### Keys/Commands:
+ - AUX is sequencer Start/Stop
+ - Pots 1-4 control "event density" (probability) of 4 instruments - values are shown on display
+ - Pot 5 sets resolution (1/2, 1, 2)
+ - Bottom row keys 1-8 are quick-keys for X/Y values - hold a key and turn encoder to change that instrument's X or Y value. You can hold multiple keys to change X/Y on multiple instruments at the same time
+ - LEDs on Keys 9-12 show trigger activity of the playing pattern
+ - Lighted Keys 13,14,16 are quick keys for ACNT/XAOS/BPM
+ - Pattern keys (black keys) can load "snapshots" of density/x/y settings
+ - F2 + Pattern saves a "snapshot" current state of that pattern. Patterns do not automatically save, this is a performance feature, allowing you to load a pattern, tweak it, then quickly load back to it's original state
+
+#### Instrument View Mode:
+- F1 + Keys 1-4 jump to Instrument View. This shows the current pattern on that instrument (over 2 pages since patterns are 32 steps) and playhead. The LED render of the pattern will update to show each page while playing
+- Top row lighted keys(A#1, C#2, D#2, F#2) are quick-keys for ACNT/X/Y/XAOS
+- First 4 keys of bottom row will not be specially lit since they are rendering the pattern, but will allow you to quickly select a different instrument
+- F2 is a quick key to jump to params page to set Note Number, MIDI Channel and BPM
+- Key 3(F#1) in Instrument View is a quick key for Midi Channel for the instrument
+- AUX-key exits Instrument View
+
+#### Midi Keyboard Mode:
+- F1 plus bottom key 16 enters into the Midi Keyboard for sending CCs or playing over the top of the sequencer. Everything works the same as mode MI
+- Hold Aux and bottom key 16 to exit out of the Midi Keyboard mode
+
+### Menu Pages
+Page 1 Event Densities:
+- `DS 1`: event density - instrument 1
+- `DS 2`: event density - instrument 2
+- `DS 3`: event density - instrument 3
+- `DS 4`: event density - instrument 4
+
+Page 2:
+- `NT 1`: note number - instrument 1
+- `NT 2`: note number - instrument 2
+- `NT 3`: note number - instrument 3
+- `NT 4`: note number - instrument 4
+
+Page 3:
+- `ACNT`: accent amount (larger number is more variation) - applies to all instruments
+- `X `: X amount for selected instrument
+- `Y `: Y amount for selected instrument
+- `XAOS`: chaos amount - applies to all instruments
+
+Page 4 - Main Mode:
+- `BPM`: tempo
+
+Page 4 - Instrument View Active:
+- `NT -`: note number for active instrument
+- `M-CHAN`: midi chanel for active instrument
+- `BPM`: tempo
+
+---
+
+## EL - Euclidian Sequencer
+
+---
+
+## OM - Organelle Mother
+
+Pretty much the same as MI, but with the following tweaks for Organelle Mother on norns/fates/raspberry-pi.
+
+- AUX key sends CC 25 (127 on press, 0 on release)
+- Encoder turn sends CC 28 (127 on CW, 0 on CCW)
+
+---
+
+## Screensaver
+After a default timeout (3 minutes), the display will be blanked and in MI Mode a "screensaver" animation will show on the LEDs. The rightmost pot (#5) can be turned to adjust the color. Touching any keys or any of the other pots will exit the screensaver.
+
+In S1/S2 the screen will blank, but there is no LED animation.
+
+---
+
+# Hardware
+
+## MIDI Switch for the mini TRS output jack connection
+
+A hardware switch on the device will let you swap between Type-A and Type-B for the hardware MIDI TRS output jack.
+
+Products That Use Type-A mini TRS Jack Connections
+- ADDAC System products
+- Arturia BeatStep (not to be confused with the BeatStep Pro)
+- Dirtywave M8
+- IK Multimedia products
+- inMusic (Akai) products
+- Korg products
+- Line 6 products
+- little Bits w5 MIDI module
+- Make Noise 0-Coast
+
+Products That Use Type-B mini TRS Jack Connections
+- Arturia BeatStep Pro
+- Faderfox products
+- Novation products
+- Polyend products
+- 1010music Original Series 1 modules, Series 2 modules, Blackbox, MX4 and Euroshield
+
+See [https://minimidi.world](https://minimidi.world) or [https://1010music.com/stereo-minijacks-midi-connections-compatibility-guide](https://1010music.com/stereo-minijacks-midi-connections-compatibility-guide) for more information
+
diff --git a/Firmware-Hexes/OMX-27-1.12.16-T32.hex b/Archive/Firmware-Hexes/OMX-27-1.12.16-T32.hex
similarity index 100%
rename from Firmware-Hexes/OMX-27-1.12.16-T32.hex
rename to Archive/Firmware-Hexes/OMX-27-1.12.16-T32.hex
diff --git a/Firmware-Hexes/OMX-27-1.12.16-T4.hex b/Archive/Firmware-Hexes/OMX-27-1.12.16-T4.hex
similarity index 100%
rename from Firmware-Hexes/OMX-27-1.12.16-T4.hex
rename to Archive/Firmware-Hexes/OMX-27-1.12.16-T4.hex
diff --git a/Firmware-Hexes/OMX-27-1.13.3-T32.hex b/Archive/Firmware-Hexes/OMX-27-1.13.3-T32.hex
similarity index 100%
rename from Firmware-Hexes/OMX-27-1.13.3-T32.hex
rename to Archive/Firmware-Hexes/OMX-27-1.13.3-T32.hex
diff --git a/Firmware-Hexes/OMX-27-1.13.3-T4.hex b/Archive/Firmware-Hexes/OMX-27-1.13.3-T4.hex
similarity index 100%
rename from Firmware-Hexes/OMX-27-1.13.3-T4.hex
rename to Archive/Firmware-Hexes/OMX-27-1.13.3-T4.hex
diff --git a/Firmware-Hexes/OMX-27-1.13.8-T32.hex b/Archive/Firmware-Hexes/OMX-27-1.13.8-T32.hex
similarity index 100%
rename from Firmware-Hexes/OMX-27-1.13.8-T32.hex
rename to Archive/Firmware-Hexes/OMX-27-1.13.8-T32.hex
diff --git a/Firmware-Hexes/OMX-27-1.13.8-T4.hex b/Archive/Firmware-Hexes/OMX-27-1.13.8-T4.hex
similarity index 100%
rename from Firmware-Hexes/OMX-27-1.13.8-T4.hex
rename to Archive/Firmware-Hexes/OMX-27-1.13.8-T4.hex
diff --git a/Firmware-Hexes/beta/OMX-27-1.12.17b2-T32.hex b/Archive/Firmware-Hexes/beta/OMX-27-1.12.17b2-T32.hex
similarity index 100%
rename from Firmware-Hexes/beta/OMX-27-1.12.17b2-T32.hex
rename to Archive/Firmware-Hexes/beta/OMX-27-1.12.17b2-T32.hex
diff --git a/Firmware-Hexes/beta/OMX-27-1.12.17b2-T4.hex b/Archive/Firmware-Hexes/beta/OMX-27-1.12.17b2-T4.hex
similarity index 100%
rename from Firmware-Hexes/beta/OMX-27-1.12.17b2-T4.hex
rename to Archive/Firmware-Hexes/beta/OMX-27-1.12.17b2-T4.hex
diff --git a/Firmware-Hexes/beta/OMX-27-1.12.17b3-T32.hex b/Archive/Firmware-Hexes/beta/OMX-27-1.12.17b3-T32.hex
similarity index 100%
rename from Firmware-Hexes/beta/OMX-27-1.12.17b3-T32.hex
rename to Archive/Firmware-Hexes/beta/OMX-27-1.12.17b3-T32.hex
diff --git a/Firmware-Hexes/beta/OMX-27-1.12.17b3-T4.hex b/Archive/Firmware-Hexes/beta/OMX-27-1.12.17b3-T4.hex
similarity index 100%
rename from Firmware-Hexes/beta/OMX-27-1.12.17b3-T4.hex
rename to Archive/Firmware-Hexes/beta/OMX-27-1.12.17b3-T4.hex
diff --git a/Firmware-Hexes/beta/OMX-27-1.12.17b4-T32.hex b/Archive/Firmware-Hexes/beta/OMX-27-1.12.17b4-T32.hex
similarity index 100%
rename from Firmware-Hexes/beta/OMX-27-1.12.17b4-T32.hex
rename to Archive/Firmware-Hexes/beta/OMX-27-1.12.17b4-T32.hex
diff --git a/Firmware-Hexes/old/OMX-27-1.0.3.hex b/Archive/Firmware-Hexes/old/OMX-27-1.0.3.hex
similarity index 100%
rename from Firmware-Hexes/old/OMX-27-1.0.3.hex
rename to Archive/Firmware-Hexes/old/OMX-27-1.0.3.hex
diff --git a/Firmware-Hexes/old/OMX-27-1.0.5.1.hex b/Archive/Firmware-Hexes/old/OMX-27-1.0.5.1.hex
similarity index 100%
rename from Firmware-Hexes/old/OMX-27-1.0.5.1.hex
rename to Archive/Firmware-Hexes/old/OMX-27-1.0.5.1.hex
diff --git a/Firmware-Hexes/old/OMX-27-1.1.0.hex b/Archive/Firmware-Hexes/old/OMX-27-1.1.0.hex
similarity index 100%
rename from Firmware-Hexes/old/OMX-27-1.1.0.hex
rename to Archive/Firmware-Hexes/old/OMX-27-1.1.0.hex
diff --git a/Firmware-Hexes/old/OMX-27-1.12.15-T32.hex b/Archive/Firmware-Hexes/old/OMX-27-1.12.15-T32.hex
similarity index 100%
rename from Firmware-Hexes/old/OMX-27-1.12.15-T32.hex
rename to Archive/Firmware-Hexes/old/OMX-27-1.12.15-T32.hex
diff --git a/Firmware-Hexes/old/OMX-27-1.12.15-T4.hex b/Archive/Firmware-Hexes/old/OMX-27-1.12.15-T4.hex
similarity index 100%
rename from Firmware-Hexes/old/OMX-27-1.12.15-T4.hex
rename to Archive/Firmware-Hexes/old/OMX-27-1.12.15-T4.hex
diff --git a/Firmware-Hexes/old/OMX-27-1.3.0-MIDI.hex b/Archive/Firmware-Hexes/old/OMX-27-1.3.0-MIDI.hex
similarity index 100%
rename from Firmware-Hexes/old/OMX-27-1.3.0-MIDI.hex
rename to Archive/Firmware-Hexes/old/OMX-27-1.3.0-MIDI.hex
diff --git a/Firmware-Hexes/old/OMX-27-1.3.0.hex b/Archive/Firmware-Hexes/old/OMX-27-1.3.0.hex
similarity index 100%
rename from Firmware-Hexes/old/OMX-27-1.3.0.hex
rename to Archive/Firmware-Hexes/old/OMX-27-1.3.0.hex
diff --git a/Firmware-Hexes/old/OMX-27-1.4.1.hex b/Archive/Firmware-Hexes/old/OMX-27-1.4.1.hex
similarity index 100%
rename from Firmware-Hexes/old/OMX-27-1.4.1.hex
rename to Archive/Firmware-Hexes/old/OMX-27-1.4.1.hex
diff --git a/Firmware-Hexes/old/OMX-27-1.4.3.hex b/Archive/Firmware-Hexes/old/OMX-27-1.4.3.hex
similarity index 100%
rename from Firmware-Hexes/old/OMX-27-1.4.3.hex
rename to Archive/Firmware-Hexes/old/OMX-27-1.4.3.hex
diff --git a/Firmware-Hexes/old/OMX-27-1.4.4.1.hex b/Archive/Firmware-Hexes/old/OMX-27-1.4.4.1.hex
similarity index 100%
rename from Firmware-Hexes/old/OMX-27-1.4.4.1.hex
rename to Archive/Firmware-Hexes/old/OMX-27-1.4.4.1.hex
diff --git a/Firmware-Hexes/old/OMX-27-1.5.0.hex b/Archive/Firmware-Hexes/old/OMX-27-1.5.0.hex
similarity index 100%
rename from Firmware-Hexes/old/OMX-27-1.5.0.hex
rename to Archive/Firmware-Hexes/old/OMX-27-1.5.0.hex
diff --git a/Firmware-Hexes/old/OMX-27-1.5.1.hex b/Archive/Firmware-Hexes/old/OMX-27-1.5.1.hex
similarity index 100%
rename from Firmware-Hexes/old/OMX-27-1.5.1.hex
rename to Archive/Firmware-Hexes/old/OMX-27-1.5.1.hex
diff --git a/Firmware-Hexes/old/OMX-27-1.6.0.hex b/Archive/Firmware-Hexes/old/OMX-27-1.6.0.hex
similarity index 100%
rename from Firmware-Hexes/old/OMX-27-1.6.0.hex
rename to Archive/Firmware-Hexes/old/OMX-27-1.6.0.hex
diff --git a/Firmware-Hexes/old/OMX-27-1.7.7.hex b/Archive/Firmware-Hexes/old/OMX-27-1.7.7.hex
similarity index 100%
rename from Firmware-Hexes/old/OMX-27-1.7.7.hex
rename to Archive/Firmware-Hexes/old/OMX-27-1.7.7.hex
diff --git a/Firmware-Hexes/old/OMX-27-1.7.8-T4.hex b/Archive/Firmware-Hexes/old/OMX-27-1.7.8-T4.hex
similarity index 100%
rename from Firmware-Hexes/old/OMX-27-1.7.8-T4.hex
rename to Archive/Firmware-Hexes/old/OMX-27-1.7.8-T4.hex
diff --git a/Archive/OMX-27-firmware/OMX-27-firmware.ino b/Archive/OMX-27-firmware/OMX-27-firmware.ino
new file mode 100644
index 00000000..e831cc0c
--- /dev/null
+++ b/Archive/OMX-27-firmware/OMX-27-firmware.ino
@@ -0,0 +1,1081 @@
+// OMX-27 MIDI KEYBOARD / SEQUENCER
+
+// v1.13.8
+// Last update: Sept 2025
+//
+// Original concept and initial code by Steven Noreyko
+// Additional code contributions:
+// Matt Boone, Steven Zydek,
+// Chris Atkins, Will Winder,
+// Michael P Jones
+//
+// Big thanks to:
+// John Park and Gerald Stevens for initial testing and feature ideas
+// mzero for immense amounts of code coaching/assistance
+// drjohn for support
+//
+
+#include
+#include
+#include "src/consts/consts.h"
+#include "src/config.h"
+#include "src/consts/colors.h"
+#include "src/midi/midi.h"
+#include "src/ClearUI/ClearUI.h"
+#include "src/modes/sequencer.h"
+#include "src/midi/noteoffs.h"
+#include "src/hardware/storage.h"
+#include "src/midi/sysex.h"
+#include "src/hardware/omx_keypad.h"
+#include "src/utils/omx_util.h"
+#include "src/utils/cvNote_util.h"
+#include "src/hardware/omx_disp.h"
+#include "src/modes/omx_mode_midi_keyboard.h"
+#include "src/modes/omx_mode_drum.h"
+#include "src/modes/omx_mode_sequencer.h"
+#include "src/modes/omx_mode_grids.h"
+#include "src/modes/omx_mode_euclidean.h"
+#include "src/modes/omx_mode_chords.h"
+#include "src/modes/omx_screensaver.h"
+#include "src/hardware/omx_leds.h"
+#include "src/utils/music_scales.h"
+
+// Allows code to compile with smallest code LTO
+extern "C"
+{
+ int _getpid() { return -1; }
+ int _kill(int pid, int sig) { return -1; }
+ int _write() { return -1; }
+}
+
+// #define RAM_MONITOR
+// #ifdef RAM_MONITOR
+// #include "src/utils/RamMonitor.h"
+// #endif
+
+OmxModeMidiKeyboard omxModeMidi;
+OmxModeDrum omxModeDrum;
+OmxModeSequencer omxModeSeq;
+#ifdef OMXMODEGRIDS
+OmxModeGrids omxModeGrids;
+#endif
+OmxModeEuclidean omxModeEuclid;
+OmxModeChords omxModeChords;
+
+OmxModeInterface *activeOmxMode;
+
+OmxScreensaver omxScreensaver;
+
+MusicScales globalScale;
+
+// storage of pot values; current is in the main loop; last value is for midi output
+int volatile currentValue[NUM_CC_POTS];
+int lastMidiValue[NUM_CC_POTS];
+
+int temp;
+
+Micros lastProcessTime;
+
+uint8_t RES;
+uint16_t AMAX;
+int V_scale;
+
+// ENCODER
+Encoder myEncoder(12, 11); // encoder pins on hardware
+const int buttonPin = 0;
+int buttonState = 1;
+Button encButton(buttonPin);
+
+// long newPosition = 0;
+// long oldPosition = -999;
+
+// KEYPAD
+// initialize an instance of custom Keypad class
+unsigned long longPressInterval = 800;
+unsigned long clickWindow = 200;
+OMXKeypad keypad(longPressInterval, clickWindow, makeKeymap(keys), rowPins, colPins, ROWS, COLS);
+
+// setup EEPROM/FRAM storage
+Storage *storage;
+SysEx *sysEx;
+
+#ifdef RAM_MONITOR
+RamMonitor ram;
+uint32_t reporttime;
+
+void report_ram_stat(const char *aname, uint32_t avalue)
+{
+ Serial.print(aname);
+ Serial.print(": ");
+ Serial.print((avalue + 512) / 1024);
+ Serial.print(" Kb (");
+ Serial.print((((float)avalue) / ram.total()) * 100, 1);
+ Serial.println("%)");
+};
+
+void report_profile_time(const char *aname, uint32_t avalue)
+{
+ Serial.print(aname);
+ Serial.print(": ");
+ Serial.print(avalue);
+ Serial.println("\n");
+};
+
+void report_ram()
+{
+ bool lowmem;
+ bool crash;
+
+ Serial.println("==== memory report ====");
+
+ report_ram_stat("free", ram.adj_free());
+ report_ram_stat("stack", ram.stack_total());
+ report_ram_stat("heap", ram.heap_total());
+
+ lowmem = ram.warning_lowmem();
+ crash = ram.warning_crash();
+ if (lowmem || crash)
+ {
+ Serial.println();
+
+ if (crash)
+ Serial.println("**warning: stack and heap crash possible");
+ else if (lowmem)
+ Serial.println("**warning: unallocated memory running low");
+ };
+
+ Serial.println();
+};
+#endif
+
+// ####### SEQUENCER LEDS #######
+
+void changeOmxMode(OMXMode newOmxmode)
+{
+ // Serial.println((String)"NewMode: " + newOmxmode);
+ sysSettings.omxMode = newOmxmode;
+ sysSettings.newmode = newOmxmode;
+
+ if (activeOmxMode != nullptr)
+ {
+ activeOmxMode->onModeDeactivated();
+ }
+
+ switch (newOmxmode)
+ {
+ case MODE_MIDI:
+ omxModeMidi.setMidiMode();
+ activeOmxMode = &omxModeMidi;
+ break;
+ case MODE_DRUM:
+ activeOmxMode = &omxModeDrum;
+ break;
+ case MODE_CHORDS:
+ activeOmxMode = &omxModeChords;
+ break;
+ case MODE_S1:
+ omxModeSeq.setSeq1Mode();
+ activeOmxMode = &omxModeSeq;
+ break;
+ case MODE_S2:
+ omxModeSeq.setSeq2Mode();
+ activeOmxMode = &omxModeSeq;
+ break;
+ case MODE_OM:
+ omxModeMidi.setOrganelleMode();
+ activeOmxMode = &omxModeMidi;
+ break;
+ case MODE_GRIDS:
+#ifdef OMXMODEGRIDS
+ activeOmxMode = &omxModeGrids;
+#endif
+ break;
+ case MODE_EUCLID:
+ activeOmxMode = &omxModeEuclid;
+ break;
+ default:
+ omxModeMidi.setMidiMode();
+ activeOmxMode = &omxModeMidi;
+ break;
+ }
+
+ activeOmxMode->onModeActivated();
+
+ omxLeds.setDirty();
+ omxDisp.setDirty();
+}
+
+// ####### END LEDS
+
+// ####### POTENTIOMETERS #######
+void readPotentimeters()
+{
+ for (int k = 0; k < potCount; k++)
+ {
+ int prevValue = potSettings.analogValues[k];
+ int prevAnalog = potSettings.analog[k]->getValue();
+
+ temp = analogRead(analogPins[k]);
+ potSettings.analog[k]->update(temp);
+
+ // read from the smoother, constrain (to account for tolerances), and map it
+ temp = potSettings.analog[k]->getValue();
+ temp = constrain(temp, potMinVal, potMaxVal);
+ temp = map(temp, potMinVal, potMaxVal, 0, 16383);
+ potSettings.hiResPotVal[k] = temp;
+
+ // map and update the value
+ potSettings.analogValues[k] = temp >> 7;
+
+ int newAnalog = potSettings.analog[k]->getValue();
+
+ // delta is way smaller on T4 - what to do??
+ int analogDelta = abs(newAnalog - prevAnalog);
+
+ // if (k == 1)
+ // {
+ // Serial.print(analogPins[k]);
+ // Serial.print(" ");
+ // Serial.print(temp);
+ // Serial.print(" ");
+ // Serial.print(potSettings.analogValues[k]);
+ // Serial.print("\n");
+ // }
+
+ if (potSettings.analog[k]->hasChanged())
+ {
+ // do stuff
+ if (sysSettings.screenSaverMode)
+ {
+ omxScreensaver.onPotChanged(k, prevValue, potSettings.analogValues[k], analogDelta);
+ }
+ // don't send pots in screensaver
+ else
+ {
+ activeOmxMode->onPotChanged(k, prevValue, potSettings.analogValues[k], analogDelta);
+ }
+ }
+ }
+}
+
+// ####### END POTENTIOMETERS #######
+
+void handleNoteOn(byte channel, byte note, byte velocity)
+{
+ if (midiSettings.midiSoftThru)
+ {
+ MM::sendNoteOnHW(note, velocity, channel);
+ }
+ if (midiSettings.midiInToCV)
+ {
+ cvNoteUtil.cvNoteOn(note);
+ }
+
+ omxScreensaver.resetCounter();
+
+ activeOmxMode->inMidiNoteOn(channel, note, velocity);
+}
+
+void handleNoteOff(byte channel, byte note, byte velocity)
+{
+ if (midiSettings.midiSoftThru)
+ {
+ MM::sendNoteOffHW(note, velocity, channel);
+ }
+
+ if (midiSettings.midiInToCV)
+ {
+ cvNoteUtil.cvNoteOff(note);
+ }
+
+ activeOmxMode->inMidiNoteOff(channel, note, velocity);
+}
+
+void handleControlChange(byte channel, byte control, byte value)
+{
+ if (midiSettings.midiSoftThru)
+ {
+ MM::sendControlChangeHW(control, value, channel);
+ }
+ // change potbank on bank select
+ if (control == 0){
+ midiSettings.isBankSelect = true;
+ potSettings.potbank = constrain(value, 0, NUM_CC_BANKS - 1);
+ omxDisp.setDirty();
+ // }else if (midiSettings.isBankSelect && control == 32){
+ // midiSettings.isBankSelect = true;
+ }else{
+ midiSettings.isBankSelect = false;
+ }
+
+ activeOmxMode->inMidiControlChange(channel, control, value);
+}
+
+// #### Inbound MIDI callbacks
+void OnNoteOn(byte channel, byte note, byte velocity)
+{
+ handleNoteOn(channel, note, velocity);
+}
+void OnNoteOff(byte channel, byte note, byte velocity)
+{
+ handleNoteOff(channel, note, velocity);
+}
+void OnControlChange(byte channel, byte control, byte value)
+{
+ handleControlChange(channel, control, value);
+}
+
+void OnSysEx(const uint8_t *data, uint16_t length, bool complete)
+{
+ sysEx->processIncomingSysex(data, length);
+}
+
+void saveHeader()
+{
+ // 1 byte for EEPROM version
+ storage->write(EEPROM_HEADER_ADDRESS + 0, EEPROM_VERSION);
+
+ // 1 byte for mode
+ storage->write(EEPROM_HEADER_ADDRESS + 1, (uint8_t)sysSettings.omxMode);
+
+ // 1 byte for the active pattern
+ storage->write(EEPROM_HEADER_ADDRESS + 2, (uint8_t)sequencer.playingPattern);
+
+ // 1 byte for Midi channel
+ uint8_t unMidiChannel = (uint8_t)(sysSettings.midiChannel - 1);
+ storage->write(EEPROM_HEADER_ADDRESS + 3, unMidiChannel);
+
+ for (int b = 0; b < NUM_CC_BANKS; b++)
+ {
+ for (int i = 0; i < NUM_CC_POTS; i++)
+ {
+ storage->write(EEPROM_HEADER_ADDRESS + 4 + i + (5 * b), pots[b][i]);
+ }
+ }
+ // Last is 28
+
+ uint8_t midiMacroChan = (uint8_t)(midiMacroConfig.midiMacroChan - 1);
+ storage->write(EEPROM_HEADER_ADDRESS + 29, midiMacroChan);
+
+ uint8_t midiMacroId = (uint8_t)midiMacroConfig.midiMacro;
+ storage->write(EEPROM_HEADER_ADDRESS + 30, midiMacroId);
+
+ uint8_t scaleRoot = (uint8_t)scaleConfig.scaleRoot;
+ storage->write(EEPROM_HEADER_ADDRESS + 31, scaleRoot);
+
+ uint8_t scalePattern = (uint8_t)scaleConfig.scalePattern;
+ storage->write(EEPROM_HEADER_ADDRESS + 32, scalePattern);
+
+ uint8_t lockScale = (uint8_t)scaleConfig.lockScale;
+ storage->write(EEPROM_HEADER_ADDRESS + 33, lockScale);
+
+ uint8_t scaleGrp16 = (uint8_t)scaleConfig.group16;
+ storage->write(EEPROM_HEADER_ADDRESS + 34, scaleGrp16);
+
+ storage->write(EEPROM_HEADER_ADDRESS + 35, midiSettings.defaultVelocity);
+
+ storage->write(EEPROM_HEADER_ADDRESS + 36, clockConfig.globalQuantizeStepIndex);
+
+ storage->write(EEPROM_HEADER_ADDRESS + 37, cvNoteUtil.triggerMode);
+
+ storage->write(EEPROM_HEADER_ADDRESS + 38, potSettings.potbank);
+}
+
+// returns true if the header contained initialized data
+// false means we shouldn't attempt to load any further information
+bool loadHeader(void)
+{
+ uint8_t version = storage->read(EEPROM_HEADER_ADDRESS + 0);
+
+ char buf[64];
+ snprintf(buf, sizeof(buf), "EEPROM Header Version is %d\n", version);
+ Serial.print(buf);
+
+ // Uninitalized EEPROM memory is filled with 0xFF
+ if (version == 0xFF)
+ {
+ // EEPROM was uninitialized
+ Serial.println("version was 0xFF");
+ return false;
+ }
+
+ if (version != EEPROM_VERSION)
+ {
+ // write an adapter if we ever need to increment the EEPROM version and also save the existing patterns
+ // for now, return false will essentially reset the state
+ Serial.println("version not matched");
+ return false;
+ }
+
+ sysSettings.omxMode = (OMXMode)storage->read(EEPROM_HEADER_ADDRESS + 1);
+
+ sequencer.playingPattern = storage->read(EEPROM_HEADER_ADDRESS + 2);
+ sysSettings.playingPattern = sequencer.playingPattern;
+
+ uint8_t unMidiChannel = storage->read(EEPROM_HEADER_ADDRESS + 3);
+ sysSettings.midiChannel = unMidiChannel + 1;
+
+ Serial.println("Loading banks");
+ for (int b = 0; b < NUM_CC_BANKS; b++)
+ {
+ for (int i = 0; i < NUM_CC_POTS; i++)
+ {
+ pots[b][i] = storage->read(EEPROM_HEADER_ADDRESS + 4 + i + (5 * b));
+ }
+ }
+
+ uint8_t midiMacroChannel = storage->read(EEPROM_HEADER_ADDRESS + 29);
+ midiMacroConfig.midiMacroChan = midiMacroChannel + 1;
+
+ uint8_t midiMacro = storage->read(EEPROM_HEADER_ADDRESS + 30);
+ midiMacroConfig.midiMacro = midiMacro;
+
+ uint8_t scaleRoot = storage->read(EEPROM_HEADER_ADDRESS + 31);
+ scaleConfig.scaleRoot = scaleRoot;
+
+ int8_t scalePattern = (int8_t)storage->read(EEPROM_HEADER_ADDRESS + 32);
+ scaleConfig.scalePattern = scalePattern;
+
+ bool lockScale = (bool)storage->read(EEPROM_HEADER_ADDRESS + 33);
+ scaleConfig.lockScale = lockScale;
+
+ bool scaleGrp16 = (bool)storage->read(EEPROM_HEADER_ADDRESS + 34);
+ scaleConfig.group16 = scaleGrp16;
+
+ globalScale.calculateScale(scaleConfig.scaleRoot, scaleConfig.scalePattern);
+
+ midiSettings.defaultVelocity = storage->read(EEPROM_HEADER_ADDRESS + 35);
+
+ clockConfig.globalQuantizeStepIndex = constrain(storage->read(EEPROM_HEADER_ADDRESS + 36), 0, kNumArpRates - 1);
+
+ cvNoteUtil.triggerMode = constrain(storage->read(EEPROM_HEADER_ADDRESS + 37), 0, 1);
+
+ potSettings.potbank = constrain(storage->read(EEPROM_HEADER_ADDRESS + 38), 0, NUM_CC_BANKS-1);
+
+ return true;
+}
+
+void savePatterns(void)
+{
+ bool isEeprom = storage->isEeprom();
+
+ int patternSize = serializedPatternSize(isEeprom);
+ int nLocalAddress = EEPROM_PATTERN_ADDRESS;
+
+ // Serial.println((String)"Seq patternSize: " + patternSize);
+ int seqPatternNum = isEeprom ? NUM_SEQ_PATTERNS_EEPROM : NUM_SEQ_PATTERNS;
+
+ for (int i = 0; i < seqPatternNum; i++)
+ {
+ auto pattern = (byte *)sequencer.getPattern(i);
+ for (int j = 0; j < patternSize; j++)
+ {
+ storage->write(nLocalAddress + j, *pattern++);
+ }
+
+ nLocalAddress += patternSize;
+ }
+
+ if (isEeprom)
+ {
+ return;
+ }
+ Serial.println((String)"nLocalAddress: " + nLocalAddress); // 5784
+
+#ifdef OMXMODEGRIDS
+ Serial.println("Saving Grids");
+
+ // Grids patterns
+ patternSize = OmxModeGrids::serializedPatternSize(isEeprom);
+ int numPatterns = OmxModeGrids::getNumPatterns();
+
+ // Serial.println((String)"OmxModeGrids patternSize: " + patternSize);
+ // Serial.println((String)"numPatterns: " + numPatterns);
+
+ for (int i = 0; i < numPatterns; i++)
+ {
+ auto pattern = (byte *)omxModeGrids.getPattern(i);
+ for (int j = 0; j < patternSize; j++)
+ {
+ storage->write(nLocalAddress + j, *pattern++);
+ }
+
+ nLocalAddress += patternSize;
+ }
+ Serial.println((String)"nLocalAddress: " + nLocalAddress); // 6008
+#endif
+
+ Serial.println("Saving Euclidean");
+ nLocalAddress = omxModeEuclid.saveToDisk(nLocalAddress, storage);
+ Serial.println((String)"nLocalAddress: " + nLocalAddress); // 7433
+
+ Serial.println("Saving Chords");
+ nLocalAddress = omxModeChords.saveToDisk(nLocalAddress, storage);
+ Serial.println((String)"nLocalAddress: " + nLocalAddress); // 10505
+
+ Serial.println("Saving Drums");
+ nLocalAddress = omxModeDrum.saveToDisk(nLocalAddress, storage);
+ Serial.println((String)"nLocalAddress: " + nLocalAddress); // 11545
+
+ Serial.println("Saving MidiFX");
+ for (uint8_t i = 0; i < NUM_MIDIFX_GROUPS; i++)
+ {
+ nLocalAddress = subModeMidiFx[i].saveToDisk(nLocalAddress, storage);
+ // Serial.println((String)"Saved: " + i);
+ // Serial.println((String)"nLocalAddress: " + nLocalAddress);
+ }
+ Serial.println((String)"nLocalAddress: " + nLocalAddress); // 11585
+
+ // Starting 11545
+ // MidiFX with nothing 11585
+ // 1 MidiFX full ARPS 11913
+ //
+ // OMX Frooze/Ran out of memory after creating 4 x 8 - 3 = 29 ARPs
+ // Maybe build in a limit of 2 or one arps per MidiFX, or just recommend users not to
+ // create 29 ARPs.
+
+ // Seq patternSize: 715
+ // nLocalAddress: 5752
+ // size of patterns: 5720
+ // OmxModeGrids patternSize: 23
+ // numPatterns: 8
+ // nLocalAddress: 5936
+ // size of grids: 184
+}
+
+void loadPatterns(void)
+{
+ bool isEeprom = storage->isEeprom();
+
+ int patternSize = serializedPatternSize(isEeprom);
+ int nLocalAddress = EEPROM_PATTERN_ADDRESS;
+
+ Serial.print("Seq patterns - nLocalAddress: ");
+ Serial.println(nLocalAddress);
+
+ int seqPatternNum = isEeprom ? NUM_SEQ_PATTERNS_EEPROM : NUM_SEQ_PATTERNS;
+
+ for (int i = 0; i < seqPatternNum; i++)
+ {
+ auto pattern = Pattern{};
+ auto current = (byte *)&pattern;
+ for (int j = 0; j < patternSize; j++)
+ {
+ *current = storage->read(nLocalAddress + j);
+ current++;
+ }
+ sequencer.patterns[i] = pattern;
+
+ nLocalAddress += patternSize;
+ }
+
+ if (isEeprom)
+ {
+ return;
+ }
+
+ Serial.print("Grids patterns - nLocalAddress: ");
+ Serial.println(nLocalAddress);
+ // 332 - eeprom size
+ // 332 * 8 = 2656
+
+ // Grids patterns
+#ifdef OMXMODEGRIDS
+ patternSize = OmxModeGrids::serializedPatternSize(isEeprom);
+ int numPatterns = OmxModeGrids::getNumPatterns();
+
+ for (int i = 0; i < numPatterns; i++)
+ {
+ auto pattern = grids::SnapShotSettings{};
+ auto current = (byte *)&pattern;
+ for (int j = 0; j < patternSize; j++)
+ {
+ *current = storage->read(nLocalAddress + j);
+ current++;
+ }
+ omxModeGrids.setPattern(i, pattern);
+ nLocalAddress += patternSize;
+ }
+#endif
+
+ Serial.print("Pattern size: ");
+ Serial.print(patternSize);
+
+ Serial.print(" - nLocalAddress: ");
+ Serial.println(nLocalAddress);
+
+ Serial.print("Loading Euclidean - ");
+ nLocalAddress = omxModeEuclid.loadFromDisk(nLocalAddress, storage);
+ Serial.println((String) "nLocalAddress: " + nLocalAddress); // 5988
+
+ Serial.print("Loading Chords - ");
+ nLocalAddress = omxModeChords.loadFromDisk(nLocalAddress, storage);
+ Serial.println((String)"nLocalAddress: " + nLocalAddress); // 5988
+
+ Serial.print("Loading Drums - ");
+ nLocalAddress = omxModeDrum.loadFromDisk(nLocalAddress, storage);
+ Serial.println((String)"nLocalAddress: " + nLocalAddress); // 5988
+
+ // Serial.println((String)"nLocalAddress: " + nLocalAddress); // 5968
+
+ Serial.print("Loading MidiFX - ");
+ for (uint8_t i = 0; i < NUM_MIDIFX_GROUPS; i++)
+ {
+ nLocalAddress = subModeMidiFx[i].loadFromDisk(nLocalAddress, storage);
+ // Serial.println((String)"Loaded: " + i);
+ // Serial.println((String)"nLocalAddress: " + nLocalAddress);
+ }
+ Serial.println((String) "nLocalAddress: " + nLocalAddress); // 5988
+
+ // with 8 note chords, 10929
+
+ // Pattern size = 715
+ // Pattern size eprom = 332
+ // Total size of patterns = 5720
+ // Total storage size = 5749
+ // Fram = 32000 = 26251 available
+ // Eeprom = 2048
+ // Eeprom rom can save 6 patterns, plus 56 bytes
+
+ // 2832 - size of 16 euclid patterns of 16 euclids
+
+ // no arps = 9905, 5 arps = 10105, 25 arps = 11505
+
+ // no arps = 10929, 5 arps = 11129, 25 arps = 12529
+ //
+}
+
+// currently saves everything ( mode + patterns )
+void saveToStorage(void)
+{
+ Serial.println("Saving to Storage...");
+ saveHeader();
+ savePatterns();
+}
+
+// currently loads everything ( mode + patterns )
+bool loadFromStorage(void)
+{
+ // This load can happen soon after Serial.begin - enable this 'wait for Serial' if you need to Serial.print during loading
+ // while( !Serial );
+
+ Serial.println("Read the header");
+ bool bContainedData = loadHeader();
+
+ if (bContainedData)
+ {
+ Serial.println("Loading patterns");
+ loadPatterns();
+ changeOmxMode(sysSettings.omxMode);
+
+ omxDisp.isDirty();
+ omxLeds.isDirty();
+ return true;
+ }
+
+ Serial.println("-- Failed to load --");
+
+ omxDisp.isDirty();
+ omxLeds.isDirty();
+
+ return false;
+}
+
+// ############## MAIN LOOP ##############
+
+void loop()
+{
+ // customKeypad.tick();
+ keypad.tick();
+ // clksTimer = 0; // TODO - didn't see this used anywhere
+
+ Micros now = micros();
+ Micros passed = now - lastProcessTime;
+ lastProcessTime = now;
+
+ sysSettings.timeElasped = passed;
+
+ seqConfig.currentFrameMicros = micros();
+ // Micros timeStart = micros();
+ activeOmxMode->loopUpdate(passed);
+ cvNoteUtil.loopUpdate(passed);
+
+ if (passed > 0) // This should always be true
+ {
+ if (sequencer.playing || omxUtil.areClocksRunning())
+ {
+ omxScreensaver.resetCounter(); // screenSaverCounter = 0;
+ }
+ omxUtil.advanceClock(activeOmxMode, passed);
+ omxUtil.advanceSteps(passed);
+ }
+
+ // DISPLAY SETUP
+ display.clearDisplay();
+
+ // ############### SLEEP MODE ###############
+ //
+ // Serial.println(screenSaverCounter);
+ omxScreensaver.updateScreenSaverState();
+ sysSettings.screenSaverMode = omxScreensaver.shouldShowScreenSaver();
+
+ // ############### POTS ###############
+ //
+ readPotentimeters();
+
+ bool omxModeChangedThisFrame = false;
+
+ // ############### EXTERNAL MODE CHANGE / SYSEX ###############
+ if ((!encoderConfig.enc_edit && (sysSettings.omxMode != sysSettings.newmode)) || sysSettings.refresh)
+ {
+ sysSettings.newmode = sysSettings.omxMode;
+ changeOmxMode(sysSettings.omxMode);
+ omxModeChangedThisFrame = true;
+
+ sequencer.playingPattern = sysSettings.playingPattern;
+ omxDisp.setDirty();
+ omxLeds.setAllLEDS(0, 0, 0);
+ omxLeds.setDirty();
+ sysSettings.refresh = false;
+ }
+
+ // ############### ENCODER ###############
+ //
+ auto u = myEncoder.update();
+ if (u.active())
+ {
+ auto amt = u.accel(1); // where 5 is the acceleration factor if you want it, 0 if you don't)
+ omxScreensaver.resetCounter(); // screenSaverCounter = 0;
+ // Serial.println(u.dir() < 0 ? "ccw " : "cw ");
+ // Serial.println(amt);
+
+ // Change Mode
+ if (encoderConfig.enc_edit)
+ {
+ // set mode
+ // int modesize = NUM_OMX_MODES;
+ sysSettings.newmode = (OMXMode)constrain(sysSettings.newmode + amt, 0, NUM_OMX_MODES - 1);
+ // omxDisp.dispMode();
+ // omxDisp.bumpDisplayTimer();
+ omxDisp.setDirty();
+ omxLeds.setDirty();
+ }
+ else
+ {
+ activeOmxMode->onEncoderChanged(u);
+ }
+ }
+ // END ENCODER
+
+ // ############### ENCODER BUTTON ###############
+ //
+ auto s = encButton.update();
+ switch (s)
+ {
+ // SHORT PRESS
+ case Button::Down: // Serial.println("Button down");
+ omxScreensaver.resetCounter(); // screenSaverCounter = 0;
+
+ // what page are we on?
+ if (sysSettings.newmode != sysSettings.omxMode && encoderConfig.enc_edit)
+ {
+ changeOmxMode(sysSettings.newmode);
+ omxModeChangedThisFrame = true;
+ seqStop();
+ omxLeds.setAllLEDS(0, 0, 0);
+ encoderConfig.enc_edit = false;
+ // omxDisp.dispMode();
+ omxDisp.setDirty();
+ }
+ else if (encoderConfig.enc_edit)
+ {
+ encoderConfig.enc_edit = false;
+ }
+
+ // Prevents toggling encoder select when entering mode
+ if (!omxModeChangedThisFrame)
+ {
+ activeOmxMode->onEncoderButtonDown();
+ }
+
+ omxDisp.setDirty();
+ break;
+
+ // LONG PRESS
+ case Button::DownLong: // Serial.println("Button downlong");
+ if (activeOmxMode->shouldBlockEncEdit())
+ {
+ activeOmxMode->onEncoderButtonDown();
+ }
+ else
+ {
+ // Enter mode change
+ encoderConfig.enc_edit = true;
+ sysSettings.newmode = sysSettings.omxMode;
+ omxLeds.setAllLEDS(0, 0, 0);
+ omxDisp.setDirty();
+ // omxDisp.dispMode();
+ }
+
+ omxDisp.setDirty();
+ break;
+ case Button::Up: // Serial.println("Button up");
+ activeOmxMode->onEncoderButtonUp();
+ break;
+ case Button::UpLong: // Serial.println("Button uplong");
+ activeOmxMode->onEncoderButtonUpLong();
+ break;
+ default:
+ break;
+ }
+ // END ENCODER BUTTON
+
+ // ############### KEY HANDLING ###############
+ //
+ while (keypad.available())
+ {
+ auto e = keypad.next();
+ int thisKey = e.key();
+ bool keyConsumed = false;
+ // int keyPos = thisKey - 11;
+ // int seqKey = keyPos + (sequencer.patternPage[sequencer.playingPattern] * NUM_STEPKEYS);
+
+ if (e.down())
+ {
+ omxScreensaver.resetCounter(); // screenSaverCounter = 0;
+ midiSettings.keyState[thisKey] = true;
+ }
+
+ if (e.down() && thisKey == 0 && encoderConfig.enc_edit)
+ {
+ // temp - save whenever the 0 key is pressed in encoder edit mode
+ omxDisp.displayMessage("Saving...");
+ omxDisp.isDirty();
+ omxDisp.showDisplay();
+ saveToStorage();
+ // Serial.println("EEPROM saved");
+ omxDisp.displayMessage("Saved State");
+ encoderConfig.enc_edit = false;
+ omxLeds.setAllLEDS(0, 0, 0);
+ activeOmxMode->onModeActivated();
+ omxDisp.isDirty();
+ omxLeds.isDirty();
+ keyConsumed = true;
+ }
+
+ if (!keyConsumed)
+ {
+ activeOmxMode->onKeyUpdate(e);
+ }
+
+ // END MODE SWITCH
+
+ if (!e.down())
+ {
+ midiSettings.keyState[thisKey] = false;
+ }
+
+ // ### LONG KEY SWITCH PRESS
+ if (e.held() && !keyConsumed)
+ {
+ // DO LONG PRESS THINGS
+ activeOmxMode->onKeyHeldUpdate(e); // Only the sequencer uses this, could probably be handled in onKeyUpdate() but keyStates are modified before this stuff happens.
+ } // END IF HELD
+
+ } // END KEYS WHILE
+
+ if (!sysSettings.screenSaverMode)
+ {
+ omxLeds.updateBlinkStates();
+ omxDisp.UpdateMessageTextTimer();
+
+ if (encoderConfig.enc_edit)
+ {
+ omxDisp.dispMode();
+ }
+ else
+ {
+ activeOmxMode->onDisplayUpdate();
+ }
+ }
+ else
+ { // if screenSaverMode
+ omxScreensaver.onDisplayUpdate();
+ }
+
+ // DISPLAY at end of loop
+ omxDisp.showDisplay();
+
+ omxLeds.showLeds();
+
+ while (MM::usbMidiRead())
+ {
+ // incoming messages - see handlers
+ }
+ while (MM::midiRead())
+ {
+ // ignore incoming messages
+ }
+
+ // Micros elapsed = micros() - timeStart;
+ // if ((timeStart - reporttime) > 2000)
+ // {
+ // report_profile_time("Elapsed", elapsed);
+ // reporttime = timeStart;
+ // // report_ram();
+ // };
+
+#ifdef RAM_MONITOR
+ uint32_t time = millis();
+
+ if ((time - reporttime) > 2000)
+ {
+ reporttime = time;
+ report_ram();
+ };
+
+ ram.run();
+#endif
+
+} // ######## END MAIN LOOP ########
+
+// ####### SETUP #######
+
+void setup()
+{
+ Serial.begin(115200);
+ // while( !Serial );
+#if T4
+ Serial.println("Teensy 4.0");
+ // Serial.println("DAC Start!");
+ dac.begin(DAC_ADDR);
+#else
+ Serial.println("Teensy 3.2");
+#endif
+ // Init Display
+ omxDisp.setup();
+
+ // Startup screen
+ omxDisp.drawStartupScreen();
+
+ // Storage
+ storage = Storage::initStorage();
+ sysEx = new SysEx(storage, &sysSettings);
+
+#ifdef RAM_MONITOR
+ ram.initialize();
+#endif
+
+ // incoming usbMIDI callbacks
+ usbMIDI.setHandleNoteOff(OnNoteOff);
+ usbMIDI.setHandleNoteOn(OnNoteOn);
+ usbMIDI.setHandleControlChange(OnControlChange);
+ usbMIDI.setHandleSystemExclusive(OnSysEx);
+
+ // clksTimer = 0; // TODO - didn't see this used anywhere
+ omxScreensaver.resetCounter();
+ // ssstep = 0;
+
+ lastProcessTime = micros();
+ omxUtil.resetClocks();
+ omxUtil.subModeClearStorage.setStoragePtr(storage);
+
+ // HW MIDI
+ MM::begin();
+
+ randomSeed(analogRead(13));
+ srand(analogRead(13));
+
+ // SET ANALOG READ resolution to teensy's 13 usable bits
+#if T4
+ analogReadResolution(10); // Teensy 4 = 10 bits
+#else
+ analogReadResolution(13); // Teensy 3.x = 13 bits
+#endif
+
+ // CV GATE pin
+ pinMode(CVGATE_PIN, OUTPUT);
+ // ENCODER BUTTON pin
+ pinMode(buttonPin, INPUT_PULLUP);
+
+ // initialize ANALOG INPUTS and ResponsiveAnalogRead
+ for (int i = 0; i < potCount; i++)
+ {
+ // potSettings.analog[i] = new ResponsiveAnalogRead(0, true, .001);
+ // potSettings.analog[i]->setAnalogResolution(1 << 13);
+ pinMode(analogPins[i], INPUT);
+ potSettings.analog[i] = new ResponsiveAnalogRead(analogPins[i], true, .001);
+
+#if T4
+ // potSettings.analog[i]->setAnalogResolution(10);
+ // potSettings.analog[i]->setActivityThreshold(8);
+#else
+ potSettings.analog[i]->setAnalogResolution(1 << 13);
+ potSettings.analog[i]->setActivityThreshold(32);
+#endif
+
+ currentValue[i] = 0;
+ lastMidiValue[i] = 0;
+ }
+
+ // set DAC Resolution CV/GATE
+ RES = 12;
+ AMAX = pow(2, RES);
+ V_scale = 64; // pow(2,(RES-7)); 4095 max
+
+#if T4
+ dac.setVoltage(0, false);
+#else
+ analogWriteResolution(RES); // set resolution for DAC
+ analogWrite(CVPITCH_PIN, 0);
+#endif
+
+ globalScale.calculateScale(scaleConfig.scaleRoot, scaleConfig.scalePattern);
+ omxModeMidi.SetScale(&globalScale);
+ omxModeDrum.SetScale(&globalScale);
+ omxModeSeq.SetScale(&globalScale);
+#ifdef OMXMODEGRIDS
+ omxModeGrids.SetScale(&globalScale);
+#endif
+ omxModeEuclid.SetScale(&globalScale);
+ omxModeChords.SetScale(&globalScale);
+
+ // Load from EEPROM
+ bool bLoaded = loadFromStorage();
+ if (!bLoaded)
+ {
+ Serial.println( "Init load fail. Reinitializing" );
+
+ // Failed to load due to initialized EEPROM or version mismatch
+ // defaults
+ // sysSettings.omxMode = DEFAULT_MODE;
+ sequencer.playingPattern = 0;
+ sysSettings.playingPattern = 0;
+ sysSettings.midiChannel = 1;
+ pots[0][0] = CC1;
+ pots[0][1] = CC2;
+ pots[0][2] = CC3;
+ pots[0][3] = CC4;
+ pots[0][4] = CC5;
+
+ omxModeSeq.initPatterns();
+
+ changeOmxMode(DEFAULT_MODE);
+ // initPatterns();
+ saveToStorage();
+ }
+
+ // Keypad
+ // customKeypad.begin();
+ keypad.begin();
+
+ // LEDs
+ omxLeds.initSetup();
+
+
+#ifdef RAM_MONITOR
+ reporttime = millis();
+#endif
+}
+
+// ####### END SETUP #######
diff --git a/Archive/OMX-27-firmware/SYSEX_SPEC.md b/Archive/OMX-27-firmware/SYSEX_SPEC.md
new file mode 100644
index 00000000..5e5583bb
--- /dev/null
+++ b/Archive/OMX-27-firmware/SYSEX_SPEC.md
@@ -0,0 +1,52 @@
+# OMX Sysex spec
+
+The OMX-27 interfaces with its editor via MIDI Sysex. This document describes the supported messages.
+
+_Work in progress, porting from 16n faderbank editor_
+
+## `0x1F` - "1nFo"
+
+Request for OMX-27 to transmit current state via sysex. No other payload.
+
+## `0x0F` - "c0nFig"
+
+"Here is my current config." Only sent by OMX-27 as an outbound message, in response to `0x1F`. Payload of 32 bytes, describing current EEPROM state.
+
+## `0x0E` - "c0nfig Edit"
+
+~~"Here is a new complete configuration for you". Payload (other than mfg header, top/tail, etc) of 80 bytes to go straight into EEPROM, according to the memory map described in `README.md`.~~ not implemented
+
+## `0x0D` - "c0nfig edit (Device options)"
+
+"Here is a new set of device options for you". Payload (other than mfg header, top/tail, etc) of 32 bytes to go straight into appropriate locations of EEPROM, according to the following map:
+```
+ // 64 bytes of data:
+ // 0 - EEPROM VERSION
+ // 1 - Current MODE
+ // 2 - Sequencer PlayingPattern
+ // 3 - MIDI mode MidiChannel
+ // 4 - 28 - Pots (x25 - 5 banks of 5 pots)
+ // 29 - MIDI Macro Channel
+ // 30 - MIDI Macro Type
+ // 31 - Scale Root
+ // 32 - Scale Pattern, -1 for chromatic
+ // 33 - Lock Scale - Bool
+ // 34 - Scale Group 16 - Bool
+ // 35 - midiSettings.defaultVelocity
+ // 36 - clockConfig.globalQuantizeStepIndex
+ // 37 - cvNoteUtil.triggerMode
+ // 38 - actvie pot bank
+
+ // XX - 63 - Not yet used
+
+```
+Example:
+`F0 7D 00 00 0D 09 00 00 00 15 16 17 18 07 1D 1E 1F 20 21 22 23 24 25 26 27 28 29 2A 2B 5B 5D 67 68 69 00 00 00 F7`
+
+## `0x0C` - "c0nfig edit (usb options)"
+
+~~"Here is a new set of USB options for you". Payload (other than mfg header, top/tail, etc) of 32 bytes to go straight into appropriate locations of EEPROM, according to the memory map described in `README.md`.~~ not implemented
+
+## `0x0B` - "c0nfig edit (trs options)"
+
+~~"Here is a new set of TRS options for you". Payload (other than mfg header, top/tail, etc) of 32 bytes to go straight into appropriate locations of EEPROM, according to the memory map described in `README.md`.~~ not implemented
diff --git a/OMX-27-firmware/build/teensy.avr.teensy31/OMX-27-firmware.ino.eep b/Archive/OMX-27-firmware/build/teensy.avr.teensy31/OMX-27-firmware.ino.eep
similarity index 100%
rename from OMX-27-firmware/build/teensy.avr.teensy31/OMX-27-firmware.ino.eep
rename to Archive/OMX-27-firmware/build/teensy.avr.teensy31/OMX-27-firmware.ino.eep
diff --git a/OMX-27-firmware/build/teensy.avr.teensy31/OMX-27-firmware.ino.elf b/Archive/OMX-27-firmware/build/teensy.avr.teensy31/OMX-27-firmware.ino.elf
similarity index 100%
rename from OMX-27-firmware/build/teensy.avr.teensy31/OMX-27-firmware.ino.elf
rename to Archive/OMX-27-firmware/build/teensy.avr.teensy31/OMX-27-firmware.ino.elf
diff --git a/OMX-27-firmware/build/teensy.avr.teensy31/OMX-27-firmware.ino.lst b/Archive/OMX-27-firmware/build/teensy.avr.teensy31/OMX-27-firmware.ino.lst
similarity index 100%
rename from OMX-27-firmware/build/teensy.avr.teensy31/OMX-27-firmware.ino.lst
rename to Archive/OMX-27-firmware/build/teensy.avr.teensy31/OMX-27-firmware.ino.lst
diff --git a/OMX-27-firmware/build/teensy.avr.teensy31/OMX-27-firmware.ino.sym b/Archive/OMX-27-firmware/build/teensy.avr.teensy31/OMX-27-firmware.ino.sym
similarity index 100%
rename from OMX-27-firmware/build/teensy.avr.teensy31/OMX-27-firmware.ino.sym
rename to Archive/OMX-27-firmware/build/teensy.avr.teensy31/OMX-27-firmware.ino.sym
diff --git a/OMX-27-firmware/build/teensy.avr.teensy40/OMX-27-firmware.ino.eep b/Archive/OMX-27-firmware/build/teensy.avr.teensy40/OMX-27-firmware.ino.eep
similarity index 100%
rename from OMX-27-firmware/build/teensy.avr.teensy40/OMX-27-firmware.ino.eep
rename to Archive/OMX-27-firmware/build/teensy.avr.teensy40/OMX-27-firmware.ino.eep
diff --git a/OMX-27-firmware/build/teensy.avr.teensy40/OMX-27-firmware.ino.elf b/Archive/OMX-27-firmware/build/teensy.avr.teensy40/OMX-27-firmware.ino.elf
similarity index 100%
rename from OMX-27-firmware/build/teensy.avr.teensy40/OMX-27-firmware.ino.elf
rename to Archive/OMX-27-firmware/build/teensy.avr.teensy40/OMX-27-firmware.ino.elf
diff --git a/OMX-27-firmware/build/teensy.avr.teensy40/OMX-27-firmware.ino.lst b/Archive/OMX-27-firmware/build/teensy.avr.teensy40/OMX-27-firmware.ino.lst
similarity index 100%
rename from OMX-27-firmware/build/teensy.avr.teensy40/OMX-27-firmware.ino.lst
rename to Archive/OMX-27-firmware/build/teensy.avr.teensy40/OMX-27-firmware.ino.lst
diff --git a/OMX-27-firmware/build/teensy.avr.teensy40/OMX-27-firmware.ino.sym b/Archive/OMX-27-firmware/build/teensy.avr.teensy40/OMX-27-firmware.ino.sym
similarity index 100%
rename from OMX-27-firmware/build/teensy.avr.teensy40/OMX-27-firmware.ino.sym
rename to Archive/OMX-27-firmware/build/teensy.avr.teensy40/OMX-27-firmware.ino.sym
diff --git a/Archive/OMX-27-firmware/feature_ideas.md b/Archive/OMX-27-firmware/feature_ideas.md
new file mode 100644
index 00000000..e392f00a
--- /dev/null
+++ b/Archive/OMX-27-firmware/feature_ideas.md
@@ -0,0 +1,229 @@
+```
+// Memory
+// Clean slate no MidiFX
+```
+==== memory report ====
+free: 29 Kb (45.9%)
+stack: 1 Kb (1.6%)
+heap: 2 Kb (2.9%)
+``
+
+// 25 Arps
+```
+==== memory report ====
+free: 17 Kb (26.7%)
+stack: 2 Kb (3.1%)
+heap: 13 Kb (20.7%)
+```
+
+// 5 Arps
+==== memory report ====
+free: 26 Kb (40.3%)
+stack: 2 Kb (3.1%)
+heap: 13 Kb (20.7%)
+
+```
+==== memory report ====
+free: 27 Kb (42.3%)
+stack: 2 Kb (3.1%)
+heap: 13 Kb (20.7%)
+```
+
+Bugs:
+Arps don't work in MidiModeception
+
+What's next:
+
+- Chord Split Mode
+- Midi Channels For Chords
+- Hold Chord Turn set chord
+- Chord Keyboard Display
+- Default chords to something playable.
+
+
+Adafruit DMA neopixel 1.3.0
+Adafruit NeoPixel 1.10.5
+
+/Applications/Teensyduino.app/Contents/Java/hardware/tools/arm/bin/../lib/gcc/arm-none-eabi/5.4.1/../../../../arm-none-eabi/lib/armv7e-m/libc_nano.a(lib_a-signalr.o): In function `_kill_r':
+Multiple libraries were found for "Adafruit_NeoPixel.h"
+signalr.c:(.text._kill_r+0xe): undefined reference to `_kill'
+ Used: /Users/quixotic7mini/Documents/Arduino/libraries/Adafruit_NeoPixel
+/Applications/Teensyduino.app/Contents/Java/hardware/tools/arm/bin/../lib/gcc/arm-none-eabi/5.4.1/../../../../arm-none-eabi/lib/armv7e-m/libc_nano.a(lib_a-signalr.o): In function `_getpid_r':
+ Not used: /Applications/Teensyduino.app/Contents/Java/hardware/teensy/avr/libraries/Adafruit_NeoPixel
+Multiple libraries were found for "MIDI.h"
+ Used: /Applications/Teensyduino.app/Contents/Java/hardware/teensy/avr/libraries/MIDI
+signalr.c:(.text._getpid_r+0x0): undefined reference to `_getpid'
+ Not used: /Users/quixotic7mini/Documents/Arduino/libraries/MIDI_Library
+Multiple libraries were found for "Adafruit_SSD1306.h"
+ Used: /Users/quixotic7mini/Documents/Arduino/libraries/Adafruit_SSD1306
+collect2: error: ld returned 1 exit status
+ Not used: /Users/quixotic7mini/Documents/Arduino/libraries/Adafruit_SSD1306_Wemos_Mini_OLED
+
+
+Ideas:
+Aux + Encoder to quickly switch modes. Modular setup in which You can combine sequencers, keyboards etc.
+
+
+"Was messing around with the Grids mode, and thought it would be nice if the four buttons that blink showing the different 'hits' could also act as mutes."
+
+- DONE - Move MidiFX easily rather than cut/paste
+
+- DONE -Quickkeys for arps.
+
+- DONE - Way to adjust midifx and play keyboard at same time.
+
+DONE - "feedback dialogs when selecting FXgroup or FX-off might be nice"
+
+- Save / recall arp settings.
+
+
+
+
+
+
+
+- S1/S2 - Active step mode
+ This would add an additional view in which steps can be turned on or off.
+ Steps that are off get ignored as if they did not exist.
+ Allows for fun performance manipulation of sequence without changing it.
+
+- S1/S2 pattern gate feature
+
+- DONE - Arpegiator
+
+
+
+- Add MidiFX to Seq, Chord, and Grids Modes
+
+- Chord LED feedback
+
+- Auto strum - Does Arp MidiFX Accomplish this? Maybe a arp submode?
+
+- Earthsea sequencer, add into midi and chord modes, simple records what you play then plays back in natural time or quantized.
+
+- Pot CC Presets
+ These would be templates for quickly setting cc's to work with a specific synth like a mm2 or digitone.
+
+ Might be easier to just have multiple banks of pot presets.
+
+ A key shortcut for changing banks would be nice.
+
+
+- Try and make UI more consistent between modes
+
+- Show something on screen to indicate you're in a submode and can press aux to exit.
+
+- Save single mode to sysex and load from sysex
+
+- Add midi channels to chords
+
+
+
+- Full manual chord note input
+
+
+
+
+
+
+Waldorf Arp notes
+
+
+The arpeggiator uses a so-called note list that can store up to 16 notes.
+
+Sort Order is set to Num Lo>Hi, the list is rearranged so that the lowest note is placed at the first position, the second lowest note at the next
+
+
+Mode
+ off
+ on
+ one shot
+ hold
+
+Step Len
+ if Length is set to legato, all arpeggio notes are played without pauses between each step and Arp Steplen therefore has no effect.
+
+
+Range
+ octaves
+
+Patterns
+
+
+x-xxx-xxx-xxx-xx
+x-x-x--xx-x-x--x
+x-x-x-xxx-x-x-xx
+x-xxx-x-x-xxx-x-
+x-x-xx-xx-x-xx-x
+xx-x-xx-xx-x-xx-
+x-x-x-x-xx-x-x-x
+x-x-x-xx-x-xx-x-
+xxx-xxx-xxx-xxx-
+xx-xx-xx-xx-xxx-
+xx-xx-xx-xx-x-x-
+xx-xx-x-xx-xx-x-
+x-x-x-x-xx-x-xxx
+x--x--x--x--x--x
+x-x-x-x-x--xx-x-
+
+Max Notes
+
+Step length
+
+Direction
+
+ up
+ down
+ alt up
+ alt down
+
+Sort Order
+ as played
+ reversed
+ Num Lo>Hi
+ Num Hi>Lo
+ Vel Lo>Hi
+ Vel Hi>Lo
+
+Velocity
+ randomize like grids?
+
+Swing?
+
+Same Note Overlap
+
+Pattern Reset
+ With Pattern Reset, you can decide if the note list is also restarted from the beginning when the rhythm pattern is reset.
+
+ If Off is selected, the note list is not restarted, so that there is no synchronization between rhythm and note list. E.g., when you have a pattern where four steps are set and you play three notes, the pattern and the note list are repeated differently.
+
+ If On is selected, the note list will be restarted as soon as the rhythm pattern is restarted.
+
+
+Arpeggiator Edit Menu Step Data
+
+Arp Accent
+
+Arp Glide
+
+Arp Step
+
+ • If * is selected (asterisk symbol), the Arpeggiator plays the step unaltered. The note list is advanced beforehand, except when you press a new chord.
+
+ • If `off` is selected (empty space), the Arpeggiator plays nothing at this step position. When Length or Steplen is set to legato, the previous step that isn’t set to Off is still held to create the legato effect. The note list is not advanced.
+
+ • If - is selected, the Arpeggiator plays the same note as it had to play in the previous step that was set to * or ˆ. With this setting, you can repeat a particular note of the note list several times. The note list is not advanced.
+
+ • If < is selected, the Arpeggiator plays the very first note of the note list. This might be interesting if you want to only play the "root note" of a chord in a bass sound. The note list is not advanced.
+
+ • If > is selected, the Arpeggiator plays the very last note of the note list. The note list is not advanced.
+
+ • If <> is selected, the Arpeggiator plays a chord with two notes, the first and the last one of the note list. This means that you have to play at least two notes to hear the effect. Otherwise, you would hear only one note anyway. The note list is not advanced.
+
+ • If (notes) is selected (notes symbol), the Arpeggiator plays a chord with all notes from the note list. This means that you have to play at least two notes to hear the effect. The note list is not advanced.
+
+ • If ? is selected, the Arpeggiator plays a random note from the note list. This doesn’t mean that it creates any random note, it only uses one note of the note list at will. The note list is not advanced.
+
+
+
+
diff --git a/Archive/OMX-27-firmware/fonts/04B_03_7pt7b.h b/Archive/OMX-27-firmware/fonts/04B_03_7pt7b.h
new file mode 100644
index 00000000..0c3d7fe6
--- /dev/null
+++ b/Archive/OMX-27-firmware/fonts/04B_03_7pt7b.h
@@ -0,0 +1,145 @@
+const uint8_t 04B_03__7pt7bBitmaps[] PROGMEM = {
+ 0x00, 0xFF, 0x0F, 0xCE, 0x72, 0x24, 0x24, 0xFF, 0x24, 0x24, 0xFF, 0x24,
+ 0x24, 0x18, 0x6E, 0x38, 0x1C, 0x7F, 0xBE, 0x18, 0x60, 0xC4, 0xC4, 0x04,
+ 0x18, 0x18, 0x20, 0x23, 0x23, 0x30, 0x30, 0xC0, 0xC0, 0xFE, 0xCC, 0x33,
+ 0x33, 0xFC, 0x27, 0x6D, 0x89, 0xD8, 0x92, 0x76, 0x9C, 0xD8, 0xC9, 0x80,
+ 0x27, 0xFE, 0x40, 0x27, 0x60, 0xFF, 0xC0, 0xF0, 0x03, 0x03, 0x04, 0x18,
+ 0x18, 0x20, 0xC0, 0xC0, 0x30, 0xCC, 0xF3, 0xCF, 0x33, 0x0C, 0xFC, 0x92,
+ 0x49, 0xFB, 0xE0, 0x41, 0xD3, 0x0F, 0xFF, 0xFB, 0xE3, 0x8E, 0x04, 0x1F,
+ 0xBE, 0x18, 0x63, 0x8E, 0xFF, 0xF1, 0x86, 0xFF, 0xFC, 0x30, 0x34, 0x1F,
+ 0xBE, 0x30, 0xCF, 0x3C, 0xCF, 0x33, 0x0C, 0xFF, 0xF0, 0x41, 0x18, 0x82,
+ 0x08, 0x30, 0xCC, 0xF3, 0xFF, 0x33, 0x0C, 0x30, 0xCC, 0xF3, 0x3C, 0xF3,
+ 0x0C, 0xC3, 0xC3, 0xF0, 0x19, 0xC9, 0x8C, 0x10, 0xE3, 0xF8, 0x01, 0xF0,
+ 0xC6, 0x08, 0x31, 0x93, 0x98, 0xFB, 0xE0, 0x41, 0x10, 0x02, 0x08, 0x3C,
+ 0x3C, 0xC3, 0xDF, 0xDF, 0xDB, 0x3C, 0x3C, 0x30, 0xCC, 0xF3, 0xFF, 0xFC,
+ 0xF3, 0xF3, 0xCC, 0xF3, 0xFF, 0x3F, 0x3C, 0x39, 0xF1, 0x8C, 0x60, 0xE7,
+ 0xF3, 0xCC, 0xF3, 0xCF, 0x3F, 0x3C, 0xFF, 0xF1, 0x8F, 0x63, 0xFF, 0xFF,
+ 0xF1, 0x8F, 0xE3, 0x18, 0x3C, 0xFC, 0x37, 0xDF, 0x33, 0xCF, 0xC7, 0x1C,
+ 0x7F, 0xFF, 0x1C, 0x71, 0xFF, 0xC8, 0x42, 0x13, 0xFF, 0x1C, 0x70, 0x41,
+ 0x07, 0x13, 0x8E, 0xC7, 0x1C, 0xBC, 0xF3, 0x2C, 0xF1, 0xC6, 0x31, 0x8C,
+ 0x63, 0xFF, 0x03, 0x83, 0xCF, 0xB3, 0xB3, 0x83, 0x83, 0x03, 0xC3, 0x1F,
+ 0x73, 0xCF, 0x1C, 0x70, 0x30, 0xCC, 0xF3, 0xCF, 0x33, 0x0C, 0xF3, 0xCC,
+ 0xF3, 0xF3, 0xCC, 0x30, 0x30, 0xCC, 0xF3, 0xCF, 0x33, 0x0C, 0x0C, 0x30,
+ 0xF3, 0xCC, 0xF3, 0xF3, 0xCC, 0xF3, 0x3C, 0xFC, 0x30, 0x14, 0x1F, 0xBE,
+ 0xFF, 0xC8, 0x42, 0x10, 0x84, 0xC7, 0x1C, 0x71, 0xC7, 0x13, 0x8E, 0xC7,
+ 0x1C, 0x72, 0xCB, 0x23, 0x0C, 0x81, 0x81, 0x99, 0x99, 0x99, 0x99, 0x66,
+ 0x66, 0xC7, 0x1C, 0x4E, 0x3B, 0x1C, 0x71, 0xC7, 0x1C, 0x4F, 0x04, 0x13,
+ 0x8E, 0xFF, 0xC6, 0x3C, 0x63, 0xFF, 0xFF, 0x6D, 0xBF, 0xC0, 0xC0, 0x20,
+ 0x18, 0x18, 0x04, 0x03, 0x03, 0xFC, 0x92, 0x7F, 0x31, 0xB2, 0xFF, 0xF0,
+ 0xD8, 0x80, 0x3C, 0xFC, 0xF3, 0x3C, 0xF0, 0xC3, 0x0F, 0x3C, 0xCF, 0x3F,
+ 0x3C, 0x3E, 0x31, 0x83, 0x9C, 0x0C, 0x33, 0xCF, 0xCF, 0x33, 0xCF, 0x3B,
+ 0x7D, 0xF8, 0x38, 0xE0, 0x18, 0xC8, 0x4F, 0x90, 0x84, 0x3C, 0xFC, 0xF3,
+ 0x3C, 0xF0, 0xCC, 0x30, 0xC3, 0x0F, 0xB1, 0xC7, 0x1C, 0x71, 0xF0, 0xFF,
+ 0x24, 0x02, 0x49, 0x27, 0x00, 0xC3, 0x0C, 0x72, 0xCB, 0xEC, 0x71, 0xFF,
+ 0xFF, 0xFC, 0xD3, 0xD3, 0xD3, 0xD3, 0xC3, 0xFB, 0x1C, 0x71, 0xC7, 0x10,
+ 0x30, 0xCC, 0xF3, 0x30, 0xC0, 0xF3, 0xCC, 0xF3, 0xF3, 0xCC, 0x30, 0xC0,
+ 0x3C, 0xFC, 0xF3, 0x3C, 0xF0, 0xC3, 0x0C, 0xCF, 0xBD, 0x8C, 0x60, 0xFB,
+ 0x81, 0xC7, 0xFB, 0xE0, 0x21, 0x3E, 0x42, 0x10, 0x63, 0xC7, 0x1C, 0x71,
+ 0x3C, 0xF0, 0xC7, 0x1C, 0x72, 0x30, 0xC0, 0x99, 0x99, 0x99, 0x66, 0x66,
+ 0x66, 0xC9, 0x8C, 0x6C, 0xE4, 0xC7, 0x1C, 0x71, 0x3C, 0xF0, 0x41, 0x38,
+ 0xFC, 0x61, 0x88, 0x23, 0xF0, 0x39, 0xC9, 0x8C, 0x10, 0x87, 0xFF, 0xFF,
+ 0xE7, 0x08, 0x31, 0x90, 0x9C, 0x30, 0xCC, 0xC0 };
+
+const GFXglyph 04B_03__7pt7bGlyphs[] PROGMEM = {
+ { 0, 1, 1, 7, 0, 0 }, // 0x20 ' '
+ { 1, 2, 8, 3, 0, -7 }, // 0x21 '!'
+ { 3, 5, 3, 7, 0, -7 }, // 0x22 '"'
+ { 5, 8, 8, 10, 1, -7 }, // 0x23 '#'
+ { 13, 6, 10, 8, 1, -7 }, // 0x24 '$'
+ { 21, 8, 8, 10, 1, -7 }, // 0x25 '%'
+ { 29, 8, 8, 10, 1, -7 }, // 0x26 '&'
+ { 37, 2, 3, 3, 0, -7 }, // 0x27 '''
+ { 38, 3, 8, 5, 1, -7 }, // 0x28 '('
+ { 41, 3, 8, 5, 1, -7 }, // 0x29 ')'
+ { 44, 5, 5, 7, 0, -7 }, // 0x2A '*'
+ { 48, 5, 4, 7, 0, -5 }, // 0x2B '+'
+ { 51, 3, 4, 5, 1, -1 }, // 0x2C ','
+ { 53, 5, 2, 7, 0, -4 }, // 0x2D '-'
+ { 55, 2, 2, 3, 0, -1 }, // 0x2E '.'
+ { 56, 8, 8, 10, 1, -7 }, // 0x2F '/'
+ { 64, 6, 8, 8, 1, -7 }, // 0x30 '0'
+ { 70, 3, 8, 5, 1, -7 }, // 0x31 '1'
+ { 73, 6, 8, 8, 1, -7 }, // 0x32 '2'
+ { 79, 6, 8, 8, 1, -7 }, // 0x33 '3'
+ { 85, 6, 8, 8, 1, -7 }, // 0x34 '4'
+ { 91, 6, 8, 8, 1, -7 }, // 0x35 '5'
+ { 97, 6, 8, 8, 1, -7 }, // 0x36 '6'
+ { 103, 6, 8, 8, 1, -7 }, // 0x37 '7'
+ { 109, 6, 8, 8, 1, -7 }, // 0x38 '8'
+ { 115, 6, 8, 8, 1, -7 }, // 0x39 '9'
+ { 121, 2, 4, 3, 0, -5 }, // 0x3A ':'
+ { 122, 2, 6, 3, 0, -5 }, // 0x3B ';'
+ { 124, 5, 8, 7, 0, -7 }, // 0x3C '<'
+ { 129, 5, 4, 7, 0, -5 }, // 0x3D '='
+ { 132, 5, 8, 7, 0, -7 }, // 0x3E '>'
+ { 137, 6, 8, 8, 1, -7 }, // 0x3F '?'
+ { 143, 8, 8, 10, 1, -7 }, // 0x40 '@'
+ { 151, 6, 8, 8, 1, -7 }, // 0x41 'A'
+ { 157, 6, 8, 8, 1, -7 }, // 0x42 'B'
+ { 163, 5, 8, 7, 0, -7 }, // 0x43 'C'
+ { 168, 6, 8, 8, 1, -7 }, // 0x44 'D'
+ { 174, 5, 8, 7, 0, -7 }, // 0x45 'E'
+ { 179, 5, 8, 7, 0, -7 }, // 0x46 'F'
+ { 184, 6, 8, 8, 1, -7 }, // 0x47 'G'
+ { 190, 6, 8, 8, 1, -7 }, // 0x48 'H'
+ { 196, 5, 8, 7, 0, -7 }, // 0x49 'I'
+ { 201, 6, 8, 8, 1, -7 }, // 0x4A 'J'
+ { 207, 6, 8, 8, 1, -7 }, // 0x4B 'K'
+ { 213, 5, 8, 7, 0, -7 }, // 0x4C 'L'
+ { 218, 8, 8, 10, 1, -7 }, // 0x4D 'M'
+ { 226, 6, 8, 8, 1, -7 }, // 0x4E 'N'
+ { 232, 6, 8, 8, 1, -7 }, // 0x4F 'O'
+ { 238, 6, 8, 8, 1, -7 }, // 0x50 'P'
+ { 244, 6, 10, 8, 1, -7 }, // 0x51 'Q'
+ { 252, 6, 8, 8, 1, -7 }, // 0x52 'R'
+ { 258, 6, 8, 8, 1, -7 }, // 0x53 'S'
+ { 264, 5, 8, 7, 0, -7 }, // 0x54 'T'
+ { 269, 6, 8, 8, 1, -7 }, // 0x55 'U'
+ { 275, 6, 8, 8, 1, -7 }, // 0x56 'V'
+ { 281, 8, 8, 10, 1, -7 }, // 0x57 'W'
+ { 289, 6, 8, 8, 1, -7 }, // 0x58 'X'
+ { 295, 6, 8, 8, 1, -7 }, // 0x59 'Y'
+ { 301, 5, 8, 7, 0, -7 }, // 0x5A 'Z'
+ { 306, 3, 8, 5, 1, -7 }, // 0x5B '['
+ { 309, 8, 8, 10, 1, -7 }, // 0x5C '\'
+ { 317, 3, 8, 5, 1, -7 }, // 0x5D ']'
+ { 320, 5, 3, 7, 0, -7 }, // 0x5E '^'
+ { 322, 6, 2, 8, 1, -1 }, // 0x5F '_'
+ { 324, 3, 3, 5, 1, -7 }, // 0x60 '`'
+ { 326, 6, 6, 8, 1, -5 }, // 0x61 'a'
+ { 331, 6, 8, 8, 1, -7 }, // 0x62 'b'
+ { 337, 5, 6, 7, 0, -5 }, // 0x63 'c'
+ { 341, 6, 8, 8, 1, -7 }, // 0x64 'd'
+ { 347, 6, 6, 8, 1, -5 }, // 0x65 'e'
+ { 352, 5, 8, 7, 0, -7 }, // 0x66 'f'
+ { 357, 6, 9, 8, 1, -5 }, // 0x67 'g'
+ { 364, 6, 8, 8, 1, -7 }, // 0x68 'h'
+ { 370, 2, 8, 3, 0, -7 }, // 0x69 'i'
+ { 372, 3, 11, 5, 1, -7 }, // 0x6A 'j'
+ { 377, 6, 8, 8, 1, -7 }, // 0x6B 'k'
+ { 383, 2, 8, 3, 0, -7 }, // 0x6C 'l'
+ { 385, 8, 6, 10, 1, -5 }, // 0x6D 'm'
+ { 391, 6, 6, 8, 1, -5 }, // 0x6E 'n'
+ { 396, 6, 6, 8, 1, -5 }, // 0x6F 'o'
+ { 401, 6, 9, 8, 1, -5 }, // 0x70 'p'
+ { 408, 6, 9, 8, 1, -5 }, // 0x71 'q'
+ { 415, 5, 6, 7, 0, -5 }, // 0x72 'r'
+ { 419, 6, 6, 8, 1, -5 }, // 0x73 's'
+ { 424, 5, 8, 7, 0, -7 }, // 0x74 't'
+ { 429, 6, 6, 8, 1, -5 }, // 0x75 'u'
+ { 434, 6, 6, 8, 1, -5 }, // 0x76 'v'
+ { 439, 8, 6, 10, 1, -5 }, // 0x77 'w'
+ { 445, 5, 6, 7, 0, -5 }, // 0x78 'x'
+ { 449, 6, 9, 8, 1, -5 }, // 0x79 'y'
+ { 456, 6, 6, 8, 1, -5 }, // 0x7A 'z'
+ { 461, 5, 8, 7, 0, -7 }, // 0x7B '{'
+ { 466, 2, 8, 3, 0, -7 }, // 0x7C '|'
+ { 468, 5, 8, 7, 0, -7 }, // 0x7D '}'
+ { 473, 6, 3, 8, 1, -7 } }; // 0x7E '~'
+
+const GFXfont 04B_03__7pt7b PROGMEM = {
+ (uint8_t *)04B_03__7pt7bBitmaps,
+ (GFXglyph *)04B_03__7pt7bGlyphs,
+ 0x20, 0x7E, 13 };
+
+// Approx. 1148 bytes
diff --git a/Archive/OMX-27-firmware/fonts/liquid_7pt7b.h b/Archive/OMX-27-firmware/fonts/liquid_7pt7b.h
new file mode 100644
index 00000000..91ba97e9
--- /dev/null
+++ b/Archive/OMX-27-firmware/fonts/liquid_7pt7b.h
@@ -0,0 +1,155 @@
+const uint8_t liquid7pt7bBitmaps[] PROGMEM = {
+ 0x00, 0xFF, 0xFC, 0x3C, 0xDE, 0xF6, 0x6C, 0x6C, 0xFF, 0xFF, 0x6C, 0xFF,
+ 0xFF, 0x6C, 0x6C, 0x18, 0x18, 0x3E, 0x3E, 0xD8, 0xD8, 0x3E, 0x3E, 0x19,
+ 0x3E, 0x3E, 0x18, 0x18, 0xC3, 0x00, 0xC3, 0x23, 0x0C, 0x03, 0x0C, 0x20,
+ 0x20, 0xD8, 0xD8, 0x20, 0x20, 0xD9, 0xC6, 0xC6, 0x39, 0x39, 0xFC, 0x27,
+ 0x6D, 0xB6, 0xC4, 0x80, 0xD8, 0x92, 0x49, 0x3B, 0x00, 0xCF, 0x32, 0x08,
+ 0xCF, 0x30, 0x67, 0xFE, 0xC6, 0x00, 0xFC, 0xFF, 0xC0, 0xF0, 0x0C, 0x30,
+ 0xC0, 0x20, 0x82, 0x00, 0xC3, 0x0C, 0x00, 0x20, 0x8C, 0xF3, 0xCF, 0x3C,
+ 0xF3, 0xCC, 0x82, 0x00, 0x63, 0x39, 0xC6, 0x31, 0x8C, 0x67, 0xFE, 0xE3,
+ 0x80, 0xC3, 0x0C, 0x02, 0x38, 0xC3, 0xFF, 0xC0, 0xFF, 0xC6, 0x36, 0x30,
+ 0x63, 0x1F, 0x38, 0x18, 0xF7, 0xBD, 0xEF, 0xFF, 0x18, 0xC6, 0xFF, 0xF1,
+ 0x8E, 0x70, 0x63, 0x1F, 0x38, 0x3C, 0xFC, 0x30, 0xE3, 0x8C, 0xF3, 0xCC,
+ 0x82, 0x00, 0xFF, 0xC2, 0x10, 0x84, 0xC6, 0x31, 0x8C, 0x20, 0x8C, 0xF3,
+ 0x20, 0x8C, 0xF3, 0xCC, 0x82, 0x00, 0x20, 0x8C, 0xF3, 0xCC, 0x33, 0xCF,
+ 0x0F, 0x8E, 0x00, 0xF3, 0xC0, 0xF0, 0xFC, 0x0C, 0x32, 0x08, 0xC0, 0x82,
+ 0x03, 0x0C, 0xFF, 0xC0, 0x0F, 0xFC, 0xC3, 0x02, 0x08, 0x0C, 0x82, 0x30,
+ 0xC0, 0x20, 0x8C, 0xF3, 0x0C, 0x02, 0x08, 0x00, 0x82, 0x00, 0x3E, 0x7F,
+ 0x1E, 0x3D, 0xFB, 0xF7, 0xE0, 0xC0, 0x70, 0xE0, 0x20, 0x8C, 0xF3, 0xCF,
+ 0x3F, 0xFF, 0xCF, 0x3C, 0xC0, 0xE7, 0x37, 0xBE, 0x73, 0x7B, 0xDF, 0x38,
+ 0x39, 0xF1, 0x8C, 0x63, 0x18, 0xC1, 0xCE, 0xE7, 0x37, 0xBD, 0xEF, 0x7B,
+ 0xDF, 0x38, 0xFF, 0xF1, 0x8E, 0x73, 0x18, 0xC7, 0xFE, 0xFF, 0xF1, 0x8E,
+ 0x73, 0x18, 0xC6, 0x30, 0x39, 0xF1, 0x8C, 0x6F, 0x7B, 0xD9, 0xCE, 0xDE,
+ 0xF7, 0xBF, 0xFF, 0x7B, 0xDE, 0xF6, 0xFF, 0xFF, 0xFC, 0xFC, 0x92, 0x49,
+ 0x24, 0xEC, 0xDE, 0xF7, 0x8E, 0x73, 0x98, 0xDE, 0xF6, 0xC6, 0x31, 0x8C,
+ 0x63, 0x18, 0xC7, 0xFE, 0xC3, 0xC3, 0xE7, 0xE7, 0xDB, 0xDB, 0xC3, 0xC3,
+ 0xC3, 0xC3, 0xC3, 0xC7, 0x8F, 0xDF, 0xBD, 0xFB, 0xF1, 0xE3, 0xC7, 0x8F,
+ 0x18, 0x20, 0x8C, 0xF3, 0xCF, 0x3C, 0xF3, 0xCC, 0x82, 0x00, 0xE7, 0x37,
+ 0xBD, 0xE3, 0x9C, 0xC6, 0x30, 0x20, 0x8C, 0xF3, 0xCF, 0x3C, 0xF3, 0xCC,
+ 0x82, 0x03, 0x0C, 0xE7, 0x37, 0xBD, 0xE3, 0x9C, 0xDE, 0xF6, 0x3C, 0xFC,
+ 0x30, 0x20, 0x80, 0xC3, 0x0F, 0x8E, 0x00, 0xFF, 0xD8, 0xC6, 0x31, 0x8C,
+ 0x63, 0x18, 0xDE, 0xF7, 0xBD, 0xEF, 0x7B, 0xD9, 0xCE, 0xDE, 0xF7, 0xBD,
+ 0xEF, 0x7C, 0xE6, 0x30, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xD9, 0xD9,
+ 0xD9, 0x26, 0x26, 0xCF, 0x3C, 0xC0, 0x20, 0x82, 0x00, 0xCF, 0x3C, 0xC0,
+ 0xCF, 0x3C, 0xF3, 0xCC, 0x02, 0x08, 0x20, 0x82, 0x00, 0xFF, 0xF0, 0xC3,
+ 0x20, 0x82, 0x30, 0xC3, 0xFF, 0xC0, 0xFF, 0x6D, 0xB6, 0xDF, 0x80, 0xC3,
+ 0x0C, 0x00, 0x20, 0x82, 0x00, 0x0C, 0x30, 0xC0, 0xFF, 0x33, 0x33, 0x33,
+ 0x3F, 0xF0, 0x20, 0x8C, 0xF3, 0xFF, 0xC0, 0xD8, 0x90, 0x39, 0xF7, 0xBD,
+ 0x9C, 0xE0, 0xC6, 0x31, 0x8E, 0x73, 0x7B, 0xDF, 0x38, 0x39, 0xF1, 0x8C,
+ 0x1C, 0xE0, 0x18, 0xC6, 0x33, 0x9F, 0x7B, 0xD9, 0xCE, 0x20, 0x8C, 0xF3,
+ 0xFF, 0xFC, 0x0F, 0x3C, 0x27, 0x6F, 0xF6, 0xDB, 0x00, 0x39, 0xF7, 0xBD,
+ 0x9C, 0xE3, 0x1F, 0x38, 0xC6, 0x31, 0x8E, 0x73, 0x7B, 0xDE, 0xF6, 0xF0,
+ 0xFF, 0xFC, 0x24, 0x02, 0x49, 0x24, 0xEC, 0xC6, 0x31, 0x8D, 0xEF, 0x9C,
+ 0xE6, 0xF6, 0xDB, 0x6D, 0xB6, 0xC4, 0x80, 0xFC, 0xFC, 0xDB, 0xDB, 0xDB,
+ 0xE7, 0x37, 0xBD, 0x80, 0x20, 0x8C, 0xF3, 0xCC, 0x82, 0x00, 0xE7, 0x37,
+ 0xBD, 0xEF, 0x7C, 0xE6, 0x30, 0x39, 0xF7, 0xBF, 0xFC, 0x63, 0x18, 0x27,
+ 0x6D, 0xB0, 0xFF, 0x73, 0xF8, 0xDB, 0xFD, 0xB1, 0x20, 0xDE, 0xF7, 0xBD,
+ 0x9C, 0xE0, 0xDE, 0xF7, 0xCE, 0x63, 0x00, 0xC1, 0xC1, 0xD9, 0xD9, 0xD9,
+ 0x26, 0x26, 0xCF, 0x32, 0x08, 0x23, 0x3C, 0xC0, 0xCF, 0x3C, 0xF3, 0xCC,
+ 0x82, 0x30, 0xC0, 0xFC, 0xFD, 0xF8, 0x0C, 0x30, 0x08, 0x20, 0x8C, 0x30,
+ 0x20, 0x82, 0x03, 0x0C, 0xFF, 0xFF, 0xFC, 0xC3, 0x00, 0x08, 0x20, 0x80,
+ 0xC3, 0x20, 0x82, 0x30, 0xC0, 0xD9, 0xB0, 0x99, 0x30 };
+
+const GFXglyph liquid7pt7bGlyphs[] PROGMEM = {
+ { 0, 1, 1, 2, 0, 0 }, // 0x20 ' '
+ { 1, 2, 11, 4, 0, -10 }, // 0x21 '!'
+ { 4, 5, 3, 7, 0, -10 }, // 0x22 '"'
+ { 6, 8, 9, 10, 0, -10 }, // 0x23 '#'
+ { 15, 8, 13, 10, 0, -10 }, // 0x24 '$'
+ { 28, 6, 9, 8, 0, -8 }, // 0x25 '%'
+ { 35, 8, 11, 10, 0, -10 }, // 0x26 '&'
+ { 46, 2, 3, 4, 0, -10 }, // 0x27 '''
+ { 47, 3, 11, 5, 0, -10 }, // 0x28 '('
+ { 52, 3, 11, 5, 0, -10 }, // 0x29 ')'
+ { 57, 6, 6, 8, 0, -10 }, // 0x2A '*'
+ { 62, 5, 5, 7, 0, -6 }, // 0x2B '+'
+ { 66, 2, 3, 4, 0, 0 }, // 0x2C ','
+ { 67, 5, 2, 7, 0, -5 }, // 0x2D '-'
+ { 69, 2, 2, 4, 0, -1 }, // 0x2E '.'
+ { 70, 6, 11, 8, 0, -10 }, // 0x2F '/'
+ { 79, 6, 11, 6, 0, -10 }, // 0x30 '0'
+ { 88, 5, 11, 6, 0, -10 }, // 0x31 '1'
+ { 95, 6, 11, 6, 0, -10 }, // 0x32 '2'
+ { 104, 5, 11, 6, 0, -10 }, // 0x33 '3'
+ { 111, 5, 11, 6, 0, -10 }, // 0x34 '4'
+ { 118, 5, 11, 6, 0, -10 }, // 0x35 '5'
+ { 125, 6, 11, 6, 0, -10 }, // 0x36 '6'
+ { 134, 5, 11, 6, 0, -10 }, // 0x37 '7'
+ { 141, 6, 11, 6, 0, -10 }, // 0x38 '8'
+ { 150, 6, 11, 6, 0, -10 }, // 0x39 '9'
+ { 159, 2, 5, 4, 0, -4 }, // 0x3A ':'
+ { 161, 2, 7, 4, 0, -4 }, // 0x3B ';'
+ { 163, 6, 9, 8, 0, -8 }, // 0x3C '<'
+ { 170, 5, 6, 7, 0, -6 }, // 0x3D '='
+ { 174, 6, 9, 8, 0, -8 }, // 0x3E '>'
+ { 181, 6, 11, 8, 0, -10 }, // 0x3F '?'
+ { 190, 7, 11, 9, 0, -10 }, // 0x40 '@'
+ { 200, 6, 11, 8, 0, -10 }, // 0x41 'A'
+ { 209, 5, 11, 7, 0, -10 }, // 0x42 'B'
+ { 216, 5, 11, 7, 0, -10 }, // 0x43 'C'
+ { 223, 5, 11, 7, 0, -10 }, // 0x44 'D'
+ { 230, 5, 11, 7, 0, -10 }, // 0x45 'E'
+ { 237, 5, 11, 7, 0, -10 }, // 0x46 'F'
+ { 244, 5, 11, 7, 0, -10 }, // 0x47 'G'
+ { 251, 5, 11, 7, 0, -10 }, // 0x48 'H'
+ { 258, 2, 11, 4, 0, -10 }, // 0x49 'I'
+ { 261, 3, 13, 5, 0, -10 }, // 0x4A 'J'
+ { 266, 5, 11, 7, 0, -10 }, // 0x4B 'K'
+ { 273, 5, 11, 7, 0, -10 }, // 0x4C 'L'
+ { 280, 8, 11, 10, 0, -10 }, // 0x4D 'M'
+ { 291, 7, 11, 9, 0, -10 }, // 0x4E 'N'
+ { 301, 6, 11, 8, 0, -10 }, // 0x4F 'O'
+ { 310, 5, 11, 7, 0, -10 }, // 0x50 'P'
+ { 317, 6, 13, 8, 0, -10 }, // 0x51 'Q'
+ { 327, 5, 11, 7, 0, -10 }, // 0x52 'R'
+ { 334, 6, 11, 8, 0, -10 }, // 0x53 'S'
+ { 343, 5, 11, 7, 0, -10 }, // 0x54 'T'
+ { 350, 5, 11, 7, 0, -10 }, // 0x55 'U'
+ { 357, 5, 11, 7, 0, -10 }, // 0x56 'V'
+ { 364, 8, 11, 10, 0, -10 }, // 0x57 'W'
+ { 375, 6, 11, 8, 0, -10 }, // 0x58 'X'
+ { 384, 6, 11, 8, 0, -10 }, // 0x59 'Y'
+ { 393, 6, 11, 8, 0, -10 }, // 0x5A 'Z'
+ { 402, 3, 11, 5, 0, -10 }, // 0x5B '['
+ { 407, 6, 11, 8, 0, -10 }, // 0x5C '\'
+ { 416, 4, 11, 6, 0, -10 }, // 0x5D ']'
+ { 422, 6, 4, 8, 0, -10 }, // 0x5E '^'
+ { 425, 5, 2, 7, 0, -1 }, // 0x5F '_'
+ { 427, 3, 4, 5, 0, -10 }, // 0x60 '`'
+ { 429, 5, 7, 9, 2, -6 }, // 0x61 'a'
+ { 434, 5, 11, 9, 2, -10 }, // 0x62 'b'
+ { 441, 5, 7, 7, 0, -6 }, // 0x63 'c'
+ { 446, 5, 11, 7, 0, -10 }, // 0x64 'd'
+ { 453, 6, 9, 8, 0, -8 }, // 0x65 'e'
+ { 460, 3, 11, 5, 0, -10 }, // 0x66 'f'
+ { 465, 5, 11, 7, 0, -6 }, // 0x67 'g'
+ { 472, 5, 11, 7, 0, -10 }, // 0x68 'h'
+ { 479, 2, 11, 4, 0, -10 }, // 0x69 'i'
+ { 482, 3, 13, 5, 0, -10 }, // 0x6A 'j'
+ { 487, 5, 11, 7, 0, -10 }, // 0x6B 'k'
+ { 494, 3, 11, 5, 0, -10 }, // 0x6C 'l'
+ { 499, 8, 5, 10, 0, -4 }, // 0x6D 'm'
+ { 504, 5, 5, 7, 0, -4 }, // 0x6E 'n'
+ { 508, 6, 7, 8, 0, -6 }, // 0x6F 'o'
+ { 514, 5, 11, 7, 0, -8 }, // 0x70 'p'
+ { 521, 5, 9, 7, 0, -6 }, // 0x71 'q'
+ { 527, 3, 7, 5, 0, -6 }, // 0x72 'r'
+ { 530, 3, 7, 5, 0, -6 }, // 0x73 's'
+ { 533, 3, 9, 5, 0, -8 }, // 0x74 't'
+ { 537, 5, 7, 7, 0, -6 }, // 0x75 'u'
+ { 542, 5, 7, 7, 0, -6 }, // 0x76 'v'
+ { 547, 8, 7, 10, 0, -6 }, // 0x77 'w'
+ { 554, 6, 7, 8, 0, -6 }, // 0x78 'x'
+ { 560, 6, 9, 8, 0, -6 }, // 0x79 'y'
+ { 567, 3, 7, 5, 0, -6 }, // 0x7A 'z'
+ { 570, 6, 13, 8, 0, -10 }, // 0x7B '{'
+ { 580, 2, 11, 4, 0, -10 }, // 0x7C '|'
+ { 583, 6, 13, 8, 0, -10 }, // 0x7D '}'
+ { 593, 7, 4, 9, 0, -8 } }; // 0x7E '~'
+
+const GFXfont liquid7pt7b PROGMEM = {
+ (uint8_t *)liquid7pt7bBitmaps,
+ (GFXglyph *)liquid7pt7bGlyphs,
+ 0x20, 0x7E, 13 };
+
+// Approx. 1269 bytes
diff --git a/Archive/OMX-27-firmware/fonts/slkscr7pt7b.h b/Archive/OMX-27-firmware/fonts/slkscr7pt7b.h
new file mode 100644
index 00000000..cab2de9d
--- /dev/null
+++ b/Archive/OMX-27-firmware/fonts/slkscr7pt7b.h
@@ -0,0 +1,152 @@
+const uint8_t slkscr7pt7bBitmaps[] PROGMEM = {
+ 0x00, 0xFF, 0xC3, 0xDE, 0xF6, 0x6C, 0x6C, 0xFF, 0x6C, 0x6C, 0xFF, 0x6C,
+ 0x6C, 0x18, 0x30, 0xF8, 0x0C, 0x07, 0x02, 0x03, 0x09, 0xF0, 0x81, 0x00,
+ 0xE6, 0xE6, 0xE6, 0x18, 0x08, 0x67, 0x67, 0x67, 0x18, 0x30, 0xFA, 0x0C,
+ 0x07, 0x10, 0x60, 0x40, 0x7C, 0x60, 0xC0, 0xFC, 0x23, 0x6D, 0xB1, 0xC8,
+ 0x92, 0x4E, 0x18, 0x18, 0xD9, 0x3E, 0x5A, 0xD9, 0x18, 0x18, 0x18, 0x18,
+ 0x18, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x38, 0xF8, 0xC0, 0x0C, 0x30, 0xC8,
+ 0x03, 0x0C, 0x30, 0x38, 0x93, 0x1E, 0x3C, 0x78, 0xF1, 0x9C, 0xE3, 0x18,
+ 0xC6, 0x31, 0x9F, 0xF8, 0x10, 0x19, 0xCC, 0x18, 0x30, 0x7F, 0xF8, 0x10,
+ 0x19, 0xC0, 0x80, 0xC0, 0x7C, 0xD9, 0xB3, 0x67, 0xF1, 0x83, 0x06, 0x0C,
+ 0xFF, 0x83, 0x07, 0xC0, 0x80, 0xC0, 0x7C, 0x39, 0x83, 0x06, 0x0F, 0x98,
+ 0xD2, 0x1C, 0xFE, 0x0C, 0x18, 0x41, 0x06, 0x0C, 0x18, 0x38, 0x93, 0x19,
+ 0xC4, 0x98, 0xD2, 0x1C, 0x38, 0x93, 0x19, 0xF0, 0x60, 0xC1, 0x9C, 0xC0,
+ 0xC0, 0x20, 0x11, 0x80, 0x0C, 0x02, 0x30, 0x40, 0x81, 0x03, 0xF8, 0x00,
+ 0x0F, 0x80, 0xC1, 0x02, 0x07, 0x00, 0x80, 0x30, 0xF0, 0x10, 0x1B, 0x80,
+ 0x00, 0x00, 0x30, 0x3E, 0x1A, 0xDB, 0xDE, 0xC0, 0xC0, 0xC0, 0x3E, 0x38,
+ 0x93, 0x1F, 0xFC, 0x78, 0xF1, 0xE3, 0xF9, 0x93, 0x1F, 0xFC, 0x78, 0xF1,
+ 0xFC, 0x38, 0x13, 0x1E, 0x0C, 0x18, 0xD0, 0x1C, 0xF9, 0x93, 0x1E, 0x3C,
+ 0x78, 0xF1, 0xFC, 0xFE, 0x31, 0xFC, 0x63, 0x1F, 0xFE, 0x31, 0xFC, 0x63,
+ 0x18, 0x3E, 0x03, 0x06, 0xFC, 0x78, 0xF1, 0x9C, 0xC7, 0x8F, 0x1F, 0xFC,
+ 0x78, 0xF1, 0xE3, 0xFF, 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x78, 0xD0, 0x1C,
+ 0xC7, 0x83, 0x67, 0x0E, 0x1B, 0x32, 0x63, 0xC6, 0x31, 0x8C, 0x63, 0x1F,
+ 0xC3, 0xC3, 0xE7, 0xDB, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xE3, 0xDB,
+ 0xCB, 0xC7, 0xC3, 0xC3, 0x38, 0x93, 0x1E, 0x3C, 0x78, 0xF1, 0x9C, 0xF9,
+ 0x93, 0x1F, 0xCC, 0x18, 0x30, 0x60, 0x38, 0x93, 0x1E, 0x3C, 0x78, 0xF1,
+ 0x9C, 0x06, 0xF9, 0x93, 0x1F, 0xCD, 0x9B, 0x36, 0x63, 0x3E, 0x03, 0x01,
+ 0xC0, 0x80, 0xC0, 0x7C, 0xFB, 0x18, 0xC6, 0x31, 0x8C, 0xC7, 0x8F, 0x1E,
+ 0x3C, 0x78, 0xD0, 0x1C, 0xC1, 0xC1, 0xC1, 0x26, 0x26, 0x26, 0x20, 0x18,
+ 0xC1, 0xD9, 0xD9, 0xD9, 0xD9, 0xD9, 0x48, 0x26, 0xC1, 0x42, 0x26, 0x18,
+ 0x08, 0x26, 0x02, 0xC1, 0xC1, 0x40, 0x26, 0x18, 0x18, 0x18, 0x18, 0x18,
+ 0xFC, 0x30, 0xC8, 0xC3, 0x0C, 0x3F, 0xFB, 0x6D, 0xB7, 0xC3, 0x0C, 0x08,
+ 0x10, 0x30, 0xC3, 0xF3, 0x33, 0x33, 0x3F, 0x21, 0x4C, 0xC0, 0xFC, 0xC8,
+ 0x80, 0x38, 0x93, 0x1F, 0xFC, 0x78, 0xF1, 0xE3, 0xF9, 0x93, 0x1F, 0xFC,
+ 0x78, 0xF1, 0xFC, 0x38, 0x13, 0x1E, 0x0C, 0x18, 0xD0, 0x1C, 0xF9, 0x93,
+ 0x1E, 0x3C, 0x78, 0xF1, 0xFC, 0xFE, 0x31, 0xFC, 0x63, 0x1F, 0xFE, 0x31,
+ 0xFC, 0x63, 0x18, 0x3E, 0x03, 0x06, 0xFC, 0x78, 0xF1, 0x9C, 0xC7, 0x8F,
+ 0x1F, 0xFC, 0x78, 0xF1, 0xE3, 0xFF, 0xFF, 0x06, 0x0C, 0x18, 0x30, 0x78,
+ 0xD0, 0x1C, 0xC7, 0x83, 0x67, 0x0E, 0x1B, 0x32, 0x63, 0xC6, 0x31, 0x8C,
+ 0x63, 0x1F, 0xC3, 0xC3, 0xE7, 0xDB, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3,
+ 0xE3, 0xDB, 0xCB, 0xC7, 0xC3, 0xC3, 0x38, 0x93, 0x1E, 0x3C, 0x78, 0xF1,
+ 0x9C, 0xF9, 0x93, 0x1F, 0xCC, 0x18, 0x30, 0x60, 0x38, 0x93, 0x1E, 0x3C,
+ 0x78, 0xF1, 0x9C, 0x06, 0xF9, 0x93, 0x1F, 0xCD, 0x9B, 0x36, 0x63, 0x3E,
+ 0x03, 0x01, 0xC0, 0x80, 0xC0, 0x7C, 0xFB, 0x18, 0xC6, 0x31, 0x8C, 0xC7,
+ 0x8F, 0x1E, 0x3C, 0x78, 0xD0, 0x1C, 0xC1, 0xC1, 0xC1, 0x26, 0x26, 0x26,
+ 0x20, 0x18, 0xC1, 0xD9, 0xD9, 0xD9, 0xD9, 0xD9, 0x48, 0x26, 0xC1, 0x42,
+ 0x26, 0x18, 0x08, 0x26, 0x02, 0xC1, 0xC1, 0x40, 0x26, 0x18, 0x18, 0x18,
+ 0x18, 0x18, 0xFC, 0x30, 0xC8, 0xC3, 0x0C, 0x3F, 0x39, 0x09, 0x82, 0x10,
+ 0x87, 0xFF, 0xFF, 0xFC, 0xF1, 0x8C, 0x13, 0x18, 0xDE, 0x26, 0xD3, 0x60 };
+
+const GFXglyph slkscr7pt7bGlyphs[] PROGMEM = {
+ { 0, 1, 1, 6, 0, 0 }, // 0x20 ' '
+ { 1, 2, 8, 6, 2, -7 }, // 0x21 '!'
+ { 3, 5, 3, 9, 2, -7 }, // 0x22 '"'
+ { 5, 8, 8, 12, 2, -7 }, // 0x23 '#'
+ { 13, 7, 12, 11, 2, -9 }, // 0x24 '$'
+ { 24, 8, 8, 12, 2, -7 }, // 0x25 '%'
+ { 32, 7, 12, 11, 2, -9 }, // 0x26 '&'
+ { 43, 2, 3, 6, 2, -7 }, // 0x27 '''
+ { 44, 3, 8, 7, 2, -7 }, // 0x28 '('
+ { 47, 3, 8, 7, 2, -7 }, // 0x29 ')'
+ { 50, 8, 8, 12, 2, -7 }, // 0x2A '*'
+ { 58, 8, 8, 12, 2, -7 }, // 0x2B '+'
+ { 66, 3, 2, 7, 2, 0 }, // 0x2C ','
+ { 67, 5, 1, 9, 2, -4 }, // 0x2D '-'
+ { 68, 2, 1, 6, 2, 0 }, // 0x2E '.'
+ { 69, 6, 8, 10, 2, -7 }, // 0x2F '/'
+ { 75, 7, 8, 11, 2, -7 }, // 0x30 '0'
+ { 82, 5, 8, 9, 2, -7 }, // 0x31 '1'
+ { 87, 7, 8, 11, 2, -7 }, // 0x32 '2'
+ { 94, 7, 8, 11, 2, -7 }, // 0x33 '3'
+ { 101, 7, 8, 10, 2, -7 }, // 0x34 '4'
+ { 108, 7, 8, 11, 2, -7 }, // 0x35 '5'
+ { 115, 7, 8, 11, 2, -7 }, // 0x36 '6'
+ { 122, 7, 8, 10, 1, -7 }, // 0x37 '7'
+ { 129, 7, 8, 11, 2, -7 }, // 0x38 '8'
+ { 136, 7, 8, 11, 2, -7 }, // 0x39 '9'
+ { 143, 2, 5, 6, 2, -6 }, // 0x3A ':'
+ { 145, 3, 6, 7, 2, -5 }, // 0x3B ';'
+ { 148, 6, 8, 10, 2, -7 }, // 0x3C '<'
+ { 154, 5, 5, 9, 2, -6 }, // 0x3D '='
+ { 158, 6, 8, 10, 2, -7 }, // 0x3E '>'
+ { 164, 7, 8, 11, 2, -7 }, // 0x3F '?'
+ { 171, 8, 8, 12, 2, -7 }, // 0x40 '@'
+ { 179, 7, 8, 11, 2, -7 }, // 0x41 'A'
+ { 186, 7, 8, 11, 2, -7 }, // 0x42 'B'
+ { 193, 7, 8, 11, 2, -7 }, // 0x43 'C'
+ { 200, 7, 8, 11, 2, -7 }, // 0x44 'D'
+ { 207, 5, 8, 9, 2, -7 }, // 0x45 'E'
+ { 212, 5, 8, 9, 2, -7 }, // 0x46 'F'
+ { 217, 7, 8, 11, 2, -7 }, // 0x47 'G'
+ { 224, 7, 8, 11, 2, -7 }, // 0x48 'H'
+ { 231, 2, 8, 6, 2, -7 }, // 0x49 'I'
+ { 233, 7, 8, 11, 2, -7 }, // 0x4A 'J'
+ { 240, 7, 8, 11, 2, -7 }, // 0x4B 'K'
+ { 247, 5, 8, 9, 2, -7 }, // 0x4C 'L'
+ { 252, 8, 8, 12, 2, -7 }, // 0x4D 'M'
+ { 260, 8, 8, 12, 2, -7 }, // 0x4E 'N'
+ { 268, 7, 8, 11, 2, -7 }, // 0x4F 'O'
+ { 275, 7, 8, 11, 2, -7 }, // 0x50 'P'
+ { 282, 7, 9, 11, 2, -7 }, // 0x51 'Q'
+ { 290, 7, 8, 11, 2, -7 }, // 0x52 'R'
+ { 297, 7, 8, 11, 2, -7 }, // 0x53 'S'
+ { 304, 5, 8, 9, 2, -7 }, // 0x54 'T'
+ { 309, 7, 8, 11, 2, -7 }, // 0x55 'U'
+ { 316, 8, 8, 12, 2, -7 }, // 0x56 'V'
+ { 324, 8, 8, 12, 2, -7 }, // 0x57 'W'
+ { 332, 8, 8, 12, 2, -7 }, // 0x58 'X'
+ { 340, 8, 8, 12, 2, -7 }, // 0x59 'Y'
+ { 348, 6, 8, 10, 2, -7 }, // 0x5A 'Z'
+ { 354, 3, 8, 7, 2, -7 }, // 0x5B '['
+ { 357, 6, 8, 10, 2, -7 }, // 0x5C '\'
+ { 363, 4, 8, 7, 1, -7 }, // 0x5D ']'
+ { 367, 6, 3, 10, 2, -9 }, // 0x5E '^'
+ { 370, 6, 1, 10, 2, 1 }, // 0x5F '_'
+ { 371, 3, 3, 7, 2, -7 }, // 0x60 '`'
+ { 373, 7, 8, 11, 2, -7 }, // 0x61 'a'
+ { 380, 7, 8, 11, 2, -7 }, // 0x62 'b'
+ { 387, 7, 8, 11, 2, -7 }, // 0x63 'c'
+ { 394, 7, 8, 11, 2, -7 }, // 0x64 'd'
+ { 401, 5, 8, 9, 2, -7 }, // 0x65 'e'
+ { 406, 5, 8, 9, 2, -7 }, // 0x66 'f'
+ { 411, 7, 8, 11, 2, -7 }, // 0x67 'g'
+ { 418, 7, 8, 11, 2, -7 }, // 0x68 'h'
+ { 425, 2, 8, 6, 2, -7 }, // 0x69 'i'
+ { 427, 7, 8, 11, 2, -7 }, // 0x6A 'j'
+ { 434, 7, 8, 11, 2, -7 }, // 0x6B 'k'
+ { 441, 5, 8, 9, 2, -7 }, // 0x6C 'l'
+ { 446, 8, 8, 12, 2, -7 }, // 0x6D 'm'
+ { 454, 8, 8, 12, 2, -7 }, // 0x6E 'n'
+ { 462, 7, 8, 11, 2, -7 }, // 0x6F 'o'
+ { 469, 7, 8, 11, 2, -7 }, // 0x70 'p'
+ { 476, 7, 9, 11, 2, -7 }, // 0x71 'q'
+ { 484, 7, 8, 11, 2, -7 }, // 0x72 'r'
+ { 491, 7, 8, 11, 2, -7 }, // 0x73 's'
+ { 498, 5, 8, 9, 2, -7 }, // 0x74 't'
+ { 503, 7, 8, 11, 2, -7 }, // 0x75 'u'
+ { 510, 8, 8, 12, 2, -7 }, // 0x76 'v'
+ { 518, 8, 8, 12, 2, -7 }, // 0x77 'w'
+ { 526, 8, 8, 12, 2, -7 }, // 0x78 'x'
+ { 534, 8, 8, 12, 2, -7 }, // 0x79 'y'
+ { 542, 6, 8, 10, 2, -7 }, // 0x7A 'z'
+ { 548, 5, 8, 9, 2, -7 }, // 0x7B '{'
+ { 553, 2, 11, 6, 2, -8 }, // 0x7C '|'
+ { 556, 5, 8, 8, 1, -7 }, // 0x7D '}'
+ { 561, 7, 3, 11, 2, -7 } }; // 0x7E '~'
+
+const GFXfont slkscr7pt7b PROGMEM = {
+ (uint8_t *)slkscr7pt7bBitmaps,
+ (GFXglyph *)slkscr7pt7bGlyphs,
+ 0x20, 0x7E, 16 };
+
+// Approx. 1236 bytes
diff --git a/Archive/OMX-27-firmware/src/ClearUI/ClearUI.h b/Archive/OMX-27-firmware/src/ClearUI/ClearUI.h
new file mode 100644
index 00000000..167fa3d4
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/ClearUI/ClearUI.h
@@ -0,0 +1,10 @@
+#pragma once
+#ifndef _INCLUDE_CLEARUI_H_
+#define _INCLUDE_CLEARUI_H_
+
+#include "ClearUI_Display.h"
+#include "ClearUI_Input.h"
+// #include "ClearUI_Field.h"
+// #include "ClearUI_Layout.h"
+
+#endif // _INCLUDE_CLEARUI_H_
diff --git a/Archive/OMX-27-firmware/src/ClearUI/ClearUI_Display.cpp b/Archive/OMX-27-firmware/src/ClearUI/ClearUI_Display.cpp
new file mode 100644
index 00000000..a796664c
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/ClearUI/ClearUI_Display.cpp
@@ -0,0 +1,277 @@
+#include "ClearUI_Display.h"
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+// #include "fonts/slkscr7pt7b.h"
+// #include "fonts/liquid_7pt7b.h"
+
+#define FONTSANS FreeSansBold9pt7b
+#define FONTMONO9 FreeMono9pt7b
+#define FONTSANS12 FreeSans12pt7b
+#define FONTSANS9BOLD FreeSansBold9pt7b
+#define FONTSANS12BOLD FreeSansBold12pt7b
+#define FONT FreeSerifBold9pt7b
+#define FONT_TT TomThumb
+#define FONT_PICO Picopixel
+#define FONT_TINY Tiny3x3a2pt7b
+#define FONT5 Org_01
+#define FONTSILK slkscr7pt7b
+#define FONTLIQUID liquid_7pt7b
+
+#define DIGIT_WIDTH 9
+#define DIGIT_HEIGHT 12
+
+#define DISPLAY_WIDTH 128
+#define DISPLAY_HEIGHT 32
+#define OLED_RST -1
+#define CLKDURING 1000000
+#define CLKAFTER 400000
+
+Adafruit_SSD1306 display = Adafruit_SSD1306(DISPLAY_WIDTH, DISPLAY_HEIGHT, &Wire, OLED_RST, CLKDURING, CLKAFTER);
+
+void initializeDisplay()
+{
+ // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
+ display.begin(SSD1306_SWITCHCAPVCC, 0x3C); // Address 0x3C for 128x32
+
+ display.cp437();
+ setRotationNormal();
+}
+
+// Display is mounted upside down on PCB
+void setRotationNormal()
+{
+ display.setRotation(2);
+}
+
+void setRotationSideways()
+{
+ display.setRotation(1);
+}
+
+void defaultText(int size)
+{
+ display.setTextSize(size);
+ display.setFont();
+}
+
+void serifText(int size)
+{
+ display.setTextSize(size);
+ display.setFont(&FONT);
+ display.setTextColor(WHITE);
+ display.setTextWrap(false);
+}
+void mono9Text(int size)
+{
+ display.setTextSize(size);
+ display.setFont(&FONTMONO9);
+ display.setTextColor(WHITE);
+ display.setTextWrap(false);
+}
+// void silkText(int size) {
+// display.setTextSize(size);
+// display.setFont(&FONTSILK);
+// display.setTextColor(WHITE);
+// display.setTextWrap(false);
+// }
+// void liquidText(int size) {
+// display.setTextSize(size);
+// display.setFont(&FONTSILK);
+// display.setTextColor(WHITE);
+// display.setTextWrap(false);
+// }
+void sans9bText(int size)
+{
+ display.setTextSize(size);
+ display.setFont(&FONTSANS9BOLD);
+ display.setTextColor(WHITE);
+ display.setTextWrap(false);
+}
+// void sans12bText(int size) {
+// display.setTextSize(size);
+// display.setFont(&FONTSANS12BOLD);
+// display.setTextColor(WHITE);
+// display.setTextWrap(false);
+// }
+
+void tinyText(int size)
+{
+ display.setTextSize(size);
+ display.setFont(&FONT_TINY);
+ display.setTextColor(WHITE);
+ display.setTextWrap(false);
+}
+void tomText(int size)
+{
+ display.setTextSize(size);
+ display.setFont(&FONT_TT);
+ display.setTextColor(WHITE);
+ display.setTextWrap(false);
+}
+void picoText(int size)
+{
+ display.setTextSize(size);
+ display.setFont(&FONT_PICO);
+ display.setTextColor(WHITE);
+ display.setTextWrap(false);
+}
+void f5Text(int size)
+{
+ display.setTextSize(size);
+ display.setFont(&FONT5);
+ display.setTextColor(WHITE);
+ display.setTextWrap(false);
+}
+
+void centerText(const char *s, int16_t x, int16_t y, uint16_t w, uint16_t h)
+{
+ int16_t bx, by;
+ uint16_t bw, bh;
+
+ display.getTextBounds(s, x, y, &bx, &by, &bw, &bh);
+ display.setCursor(
+ x + (x - bx) + (w - bw) / 2,
+ y + (y - by) + (h - bh) / 2);
+ display.print(s);
+}
+
+void centerNumber(unsigned int n,
+ uint16_t x, uint16_t y, uint16_t w, uint16_t h)
+{
+ char buf[8];
+ utoa(n, buf, 10);
+ centerText(buf, x, y, w, h);
+}
+
+namespace
+{
+ const unsigned long saverStartDelay = 15 * 60 * 1000;
+
+ unsigned long saverStartAt = 0;
+ bool saverRunning = false;
+
+ size_t savedSize = 0;
+ uint8_t *savedDisplay = NULL;
+
+ unsigned long saverBumpTime;
+ int16_t saverPhase;
+
+ const unsigned char wipePattern[] = { // 24 x 32
+ B00000010, B10101011, B11111111,
+ B00000010, B10101011, B11111111,
+ B00000010, B10101011, B11111111,
+ B00000101, B01010111, B11111110,
+ B00000101, B01010111, B11111110,
+ B00000101, B01010111, B11111110,
+ B00000101, B01010111, B11111110,
+ B00000101, B01010111, B11111110,
+ B00000101, B01010111, B11111110,
+ B00001010, B10101111, B11111100,
+ B00001010, B10101111, B11111100,
+ B00001010, B10101111, B11111100,
+ B00001010, B10101111, B11111100,
+ B00001010, B10101111, B11111100,
+ B00010101, B01011111, B11111000,
+ B00010101, B01011111, B11111000,
+ B00010101, B01011111, B11111000,
+ B00010101, B01011111, B11111000,
+ B00010101, B01011111, B11111000,
+ B00101010, B10111111, B11110000,
+ B00101010, B10111111, B11110000,
+ B00101010, B10111111, B11110000,
+ B00101010, B10111111, B11110000,
+ B00101010, B10111111, B11110000,
+ B00101010, B10111111, B11110000,
+ B01010101, B01111111, B11100000,
+ B01010101, B01111111, B11100000,
+ B01010101, B01111111, B11100000,
+ B01010101, B01111111, B11100000,
+ B01010101, B01111111, B11100000,
+ B10101010, B11111111, B11000000,
+ B10101010, B11111111, B11000000,
+ };
+}
+
+bool updateSaver(bool redrawn)
+{
+ auto now = millis();
+
+ if (redrawn)
+ {
+ saverStartAt = now + saverStartDelay;
+ saverRunning = false;
+ return false;
+ }
+
+ if (now < saverStartAt)
+ return false;
+
+ if (!saverRunning)
+ {
+ if (!savedDisplay)
+ {
+ savedSize = DISPLAY_WIDTH * ((DISPLAY_HEIGHT + 7) / 8);
+ savedDisplay = (uint8_t *)malloc(savedSize);
+ }
+ memcpy(savedDisplay, display.getBuffer(), savedSize);
+ saverRunning = true;
+ saverPhase = 0;
+ saverBumpTime = now - 1;
+ }
+
+ if (now > saverBumpTime)
+ {
+ display.clearDisplay();
+ display.drawBitmap(saverPhase - 24, 0, wipePattern, 24, 32, WHITE);
+
+ auto d = display.getBuffer();
+ auto s = savedDisplay;
+ for (auto n = savedSize; n > 0; --n)
+ *d++ &= *s++;
+
+ display.display();
+
+ saverPhase += 2;
+ if (saverPhase >= DISPLAY_WIDTH + 24)
+ {
+ saverPhase = 0;
+ saverBumpTime += 2000; // pause between swipes
+ }
+ saverBumpTime += 50; // speed of swipe
+ }
+ return true;
+}
+
+void dumpDisplayPBM(Print &stream)
+{
+ stream.println("");
+ stream.println("P1");
+
+ auto w = display.width();
+ auto h = display.height();
+
+ stream.print(w);
+ stream.print(' ');
+ stream.println(h);
+
+ for (auto j = 0; j < h; ++j)
+ {
+ for (auto i = 0; i < w; ++i)
+ {
+ stream.print(display.getPixel(i, j) == WHITE ? " 0" : " 1");
+ // 1 is black in PBM
+ }
+ stream.println("");
+ }
+ stream.println("");
+}
diff --git a/Archive/OMX-27-firmware/src/ClearUI/ClearUI_Display.h b/Archive/OMX-27-firmware/src/ClearUI/ClearUI_Display.h
new file mode 100644
index 00000000..6c26dc6f
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/ClearUI/ClearUI_Display.h
@@ -0,0 +1,44 @@
+#pragma once
+#ifndef _INCLUDE_CLEARUI_DISPLAY_H_
+#define _INCLUDE_CLEARUI_DISPLAY_H_
+
+#include
+
+#define WHITE SSD1306_WHITE
+#define BLACK SSD1306_BLACK
+
+extern Adafruit_SSD1306 display;
+
+void initializeDisplay();
+
+void setRotationSideways();
+void setRotationNormal();
+
+void defaultText(int size);
+void serifText(int size);
+void mono9Text(int size);
+void silkText(int size);
+void liquidText(int size);
+void sans9bText(int size);
+
+void tomText(int size);
+void picoText(int size);
+void tinyText(int size);
+void f5Text(int size);
+
+void centerText(const char *s, int16_t x, int16_t y, uint16_t w, uint16_t h);
+
+void centerNumber(unsigned int n,
+ uint16_t x, uint16_t y, uint16_t w, uint16_t h);
+
+template
+void centerNumber(const N &n, uint16_t x, uint16_t y, uint16_t w, uint16_t h)
+{
+ centerNumber(static_cast(n), x, y, w, h);
+}
+
+bool updateSaver(bool);
+
+void dumpDisplayPBM(Print &stream);
+
+#endif // _INCLUDE_CLEARUI_DISPLAY_H_
diff --git a/Archive/OMX-27-firmware/src/ClearUI/ClearUI_Input.cpp b/Archive/OMX-27-firmware/src/ClearUI/ClearUI_Input.cpp
new file mode 100644
index 00000000..ad988494
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/ClearUI/ClearUI_Input.cpp
@@ -0,0 +1,156 @@
+#include "ClearUI_Input.h"
+
+#include
+
+Encoder::Encoder(uint32_t pinA, uint32_t pinB)
+ : pinA(pinA), pinB(pinB)
+{
+ pinMode(pinA, INPUT_PULLUP);
+ pinMode(pinB, INPUT_PULLUP);
+ a = digitalRead(pinA);
+ b = digitalRead(pinB);
+ quads = 0;
+ lastUpdate = 0;
+}
+
+Encoder::Update Encoder::update()
+{
+ int newA = digitalRead(pinA);
+ int newB = digitalRead(pinB);
+
+ int16_t dir = 0;
+
+ if (newA != a || newB != b)
+ {
+ if (newA == a)
+ {
+ quads += (newA == newB) ? 1 : -1;
+ }
+ else if (newB == b)
+ {
+ quads += (newA != newB) ? 1 : -1;
+ }
+
+ a = newA;
+ b = newB;
+
+ if (a && b)
+ {
+ if (quads > 1)
+ {
+ dir = 1;
+ }
+ else if (quads < -1)
+ {
+ dir = -1;
+ }
+ quads = 0;
+ }
+ }
+
+ int16_t speedup = 0;
+ if (dir != 0)
+ {
+ auto now = millis();
+ auto delta = now - lastUpdate;
+ lastUpdate = now;
+
+ if (delta < 20)
+ speedup = 2;
+ else if (delta < 50)
+ speedup = 1;
+ }
+ return Update(dir, speedup);
+}
+
+Button::Button(uint32_t pin)
+ : pin(pin)
+{
+ pinMode(pin, INPUT_PULLUP); // 1 is off, 0 is pressed
+ lastRead = -1; // will cause first update to always set it
+ validAtTime = 0;
+
+ state = Up;
+ longAtTime = 0;
+}
+
+Button::State Button::update()
+{
+ int read = digitalRead(pin);
+ if (read != lastRead)
+ {
+ // pin changed, wait for it to be stable
+ lastRead = read;
+ validAtTime = millis() + validAtTimeDelay;
+ return NoChange;
+ }
+
+ uint32_t now = millis();
+ if (now < validAtTime)
+ {
+ // pin stable, not not long enough
+ return NoChange;
+ }
+
+ State prevState = state;
+
+ switch (state)
+ {
+ case Up:
+ case UpLong:
+ if (lastRead == LOW)
+ {
+ state = Down;
+ longAtTime = now + longDownTimeout;
+ }
+ break;
+
+ case Down:
+ if (lastRead == LOW)
+ { // still down?
+ if (now > longAtTime)
+ {
+ state = DownLong;
+ break;
+ }
+ }
+ // fall through
+
+ case DownLong:
+ if (lastRead == HIGH)
+ {
+ state = (prevState == DownLong) ? UpLong : Up;
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ return (state != prevState) ? state : NoChange;
+}
+
+IdleTimeout::IdleTimeout(unsigned long period)
+ : idle(true), period(period)
+{
+}
+
+void IdleTimeout::activity()
+{
+ idle = false;
+ idleAtTime = millis() + period;
+}
+
+bool IdleTimeout::update()
+{
+ if (idle)
+ return false;
+
+ if (millis() > idleAtTime)
+ {
+ idle = true;
+ return true;
+ }
+
+ return false;
+}
diff --git a/Archive/OMX-27-firmware/src/ClearUI/ClearUI_Input.h b/Archive/OMX-27-firmware/src/ClearUI/ClearUI_Input.h
new file mode 100644
index 00000000..c820690c
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/ClearUI/ClearUI_Input.h
@@ -0,0 +1,95 @@
+#pragma once
+#ifndef _INCLUDE_CLEARUI_INPUT_H_
+#define _INCLUDE_CLEARUI_INPUT_H_
+
+#include
+
+class Encoder
+{
+public:
+ Encoder(uint32_t pinA, uint32_t pinB);
+
+ struct Update
+ {
+ public:
+ inline bool active() const { return _dir != 0; }
+ inline int dir() const { return _dir; }
+ // -1 for CCW, 0 for no motion, and 1 for CW
+ inline int accel(int rate) const { return _dir + _dir * _speedup * rate; }
+
+ private:
+ Update(int16_t dir, int16_t speedup) : _dir(dir), _speedup(speedup) {}
+ int16_t _dir;
+ int16_t _speedup;
+
+ friend class Encoder;
+ };
+
+ Update update();
+
+private:
+ const uint32_t pinA;
+ const uint32_t pinB;
+
+ int a;
+ int b;
+
+ int quads;
+
+ unsigned long lastUpdate;
+};
+
+/* Buttons follow one of two cycles:
+
+ Down -> Up
+or
+ Down -> DownLong -> UpLong
+
+ There may be any number of NoChange states anywhere during these.
+*/
+
+class Button
+{
+public:
+ enum State
+ {
+ NoChange = 0, // returned from update() if no change
+ Down,
+ DownLong,
+ Up,
+ UpLong
+ };
+
+ Button(uint32_t pin);
+ State update(); // reports any action
+ inline bool active()
+ {
+ return state == Down || state == DownLong;
+ }
+
+private:
+ const uint32_t pin;
+
+ int lastRead;
+ unsigned long validAtTime;
+ unsigned long validAtTimeDelay = 50;
+
+ State state;
+ unsigned long longAtTime;
+ unsigned long longDownTimeout = 1250;
+};
+
+class IdleTimeout
+{
+public:
+ IdleTimeout(unsigned long period);
+ void activity();
+ bool update(); // returns true on start of idle
+
+private:
+ bool idle;
+ const unsigned long period;
+ unsigned long idleAtTime;
+};
+
+#endif // _INCLUDE_CLEARUI_INPUT_H_
diff --git a/Archive/OMX-27-firmware/src/config.cpp b/Archive/OMX-27-firmware/src/config.cpp
new file mode 100644
index 00000000..96ae5bec
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/config.cpp
@@ -0,0 +1,128 @@
+#include "config.h"
+#include "consts/consts.h"
+
+const OMXMode DEFAULT_MODE = MODE_MIDI;
+const uint8_t EEPROM_VERSION = 36;
+
+// v30 - adds storage to header for velocity
+// v31 - adds storage for drums
+// v32 - adds mfx chord saves
+// v33 - adds mfx selector saves
+// v34 - adds mfx repeat saves
+// v35 - adds quantize rate to arps, added global quant rate to header
+// v36 - adds stuff to randomizer
+
+// DEFINE CC NUMBERS FOR POTS // CCS mapped to Organelle Defaults
+const int CC1 = 21;
+const int CC2 = 22;
+const int CC3 = 23;
+const int CC4 = 24;
+const int CC5 = 7; // change to 25 for EYESY Knob 5
+
+const int CC_AUX = 25; // Mother mode - AUX key
+const int CC_OM1 = 26; // Mother mode - enc switch
+const int CC_OM2 = 28; // Mother mode - enc turn
+
+const int LED_BRIGHTNESS = 50;
+
+// DONT CHANGE ANYTHING BELOW HERE
+
+const int LED_PIN = 14;
+const int LED_COUNT = 27;
+
+#if DEV
+const int analogPins[] = {23, 22, 21, 20, 16}; // DEV/beta boards
+const byte DAC_ADDR = 0x62;
+#elif MIDIONLY
+const int analogPins[] = {23, 22, 21, 20, 16}; // on MIDI only boards - {23,A10,21,20,16} on Bodged MIDI boards
+const byte DAC_ADDR = 0x60;
+#elif T4
+const int analogPins[] = {23, 22, 21, 20, 16}; // on 2.0
+const byte DAC_ADDR = 0x60;
+#else
+const int analogPins[] = {34, 22, 21, 20, 16}; // on 1.0
+const byte DAC_ADDR = 0x60;
+#endif
+
+const int potCount = NUM_CC_POTS;
+
+int pots[NUM_CC_BANKS][NUM_CC_POTS] = {
+ {CC1, CC2, CC3, CC4, CC5},
+ {29, 30, 31, 32, 33},
+ {34, 35, 36, 37, 38},
+ {39, 40, 41, 42, 43},
+ {91, 93, 103, 104, 7}}; // the MIDI CC (continuous controller) for each analog input
+
+int potMinVal = 0;
+#if T4
+int potMaxVal = 1019; // T4 = 1019 // T3.2 = 8191;
+#else
+int potMaxVal = 8191; // T4 = 1019 // T3.2 = 8191;
+#endif
+
+const int gridh = 32;
+const int gridw = 128;
+const int PPQ = 96; // Pulses Per Quarter note
+
+const uint32_t secs2micros = 1000000;
+
+
+const char *mfxOffMsg = "MidiFX are Off";
+const char *mfxArpEditMsg = "Arp Edit";
+const char *mfxPassthroughEditMsg = "MFX Quickedit";
+const char *exitMsg = "Exit";
+const char *paramOffMsg = "OFF";
+const char *paramOnMsg = "ON";
+
+const char *modes[] = {"MI", "DRUM", "CH", "S1", "S2", "GR", "EL", "OM"};
+const char *macromodes[] = {"Off", "M8", "NRN", "DEL"};
+const int nummacromodes = 3;
+
+float multValues[] = {.25, .5, 1, 2, 4, 8, 16};
+const char *mdivs[] = {"1/64", "1/32", "1/16", "1/8", "1/4", "1/2", "W"};
+
+const float kNoteLengths[] = {0.10, 0.25, 0.5, 0.75, 1, 1.5, 2, 4, 8, 16};
+const uint8_t kNumNoteLengths = 10;
+
+const uint8_t kArpRates[] = {1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24, 32, 40, 48, 64};
+const uint8_t kNumArpRates = 16;
+
+String tempString = "12345";
+String tempStrings[8] = {"12345", "12345", "12345", "12345", "12345", "12345", "12345", "12345"};
+
+// KEY SWITCH ROWS/COLS
+
+// Map the keys
+char keys[ROWS][COLS] = {
+ {0, 1, 2, 3, 4, 5},
+ {6, 7, 8, 9, 10, 26},
+ {11, 12, 13, 14, 15, 24},
+ {16, 17, 18, 19, 20, 25},
+ {22, 23, 21}};
+byte rowPins[ROWS] = {6, 4, 3, 5, 2}; // row pins for key switches
+byte colPins[COLS] = {7, 8, 10, 9, 15, 17}; // column pins for key switches
+
+// KEYBOARD MIDI NOTE LAYOUT
+const int notes[] = {0,
+ 61, 63, 66, 68, 70, 73, 75, 78, 80, 82,
+ 59, 60, 62, 64, 65, 67, 69, 71, 72, 74, 76, 77, 79, 81, 83, 84};
+
+const int steps[] = {0,
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
+ 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26};
+
+const int midiKeyMap[] = {12, 1, 13, 2, 14, 15, 3, 16, 4, 17, 5, 18, 19, 6, 20, 7, 21, 22, 8, 23, 9, 24, 10, 25, 26};
+
+Adafruit_MCP4725 dac;
+SysSettings sysSettings;
+PotSettings potSettings;
+MidiConfig midiSettings;
+MidiMacroConfig midiMacroConfig;
+EncoderConfig encoderConfig;
+ClockConfig clockConfig;
+SequencerConfig seqConfig;
+ColorConfig colorConfig;
+ScaleConfig scaleConfig;
+
+// MidiPage midiPageParams;
+// SequencerPage seqPageParams;
diff --git a/Archive/OMX-27-firmware/src/config.h b/Archive/OMX-27-firmware/src/config.h
new file mode 100644
index 00000000..d5179673
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/config.h
@@ -0,0 +1,473 @@
+#pragma once
+
+#ifndef OMX_CONFIG_DONE
+#define OMX_CONFIG_DONE // prevent redifinition pragma once should handle though.
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "consts/colors.h"
+
+// #include
+
+/* * firmware metadata */
+// OMX_VERSION = 1.13.8
+const int MAJOR_VERSION = 1;
+const int MINOR_VERSION = 13;
+const int POINT_VERSION = 8;
+
+// 1.13.2 - Adds CV Trigger modes for legato and regtrig
+// 1.13.3 - Bugfix for CV Trigger modes
+// 1.13.5 - Bugfix for grids T4 pots
+// 1.13.6 - start/stop midi fixes in grids, sysex tweaks for pot banks
+// 1.13.8 - option to send midi all the time or not
+
+const int DEVICE_ID = 2;
+
+// DAC
+extern Adafruit_MCP4725 dac;
+
+enum OMXMode
+{
+ MODE_MIDI = 0,
+ MODE_DRUM,
+ MODE_CHORDS,
+ MODE_S1,
+ MODE_S2,
+ MODE_GRIDS,
+ MODE_EUCLID,
+ MODE_OM,
+
+ NUM_OMX_MODES
+};
+
+enum MIDIFXTYPE
+{
+ MIDIFX_NONE,
+ MIDIFX_CHANCE,
+ MIDIFX_TRANSPOSE,
+ MIDIFX_RANDOMIZER,
+ MIDIFX_SELECTOR,
+ MIDIFX_CHORD,
+ MIDIFX_HARMONIZER,
+ MIDIFX_SCALER,
+ MIDIFX_MONOPHONIC,
+ MIDIFX_REPEAT,
+ MIDIFX_ARP,
+ MIDIFX_COUNT
+};
+
+extern const OMXMode DEFAULT_MODE;
+
+enum FUNCKEYMODE
+{
+ FUNCKEYMODE_NONE, // No function keys
+ FUNCKEYMODE_F1, // F1 held
+ FUNCKEYMODE_F2, // F2 held
+ FUNCKEYMODE_F3 // F1 + F3 held
+};
+
+// Increment this when data layout in EEPROM changes. May need to write version upgrade readers when this changes.
+extern const uint8_t EEPROM_VERSION;
+
+#define EEPROM_HEADER_ADDRESS 0
+#define EEPROM_HEADER_SIZE 40
+#define EEPROM_PATTERN_ADDRESS 64
+
+#define TRACKED_CV_SIZE 16 //
+
+// next address 1104 (was 1096 before clock)
+
+extern const byte DAC_ADDR;
+
+// DEFINE CC NUMBERS FOR POTS // CCS mapped to Organelle Defaults
+extern const int CC1;
+extern const int CC2;
+extern const int CC3;
+extern const int CC4;
+extern const int CC5; // change to 25 for EYESY Knob 5
+
+extern const int CC_AUX; // Mother mode - AUX key
+extern const int CC_OM1; // Mother mode - enc switch
+extern const int CC_OM2; // Mother mode - enc turn
+
+extern const int LED_BRIGHTNESS;
+
+// DONT CHANGE ANYTHING BELOW HERE
+
+extern const int LED_PIN;
+extern const int LED_COUNT;
+
+// POTS/ANALOG INPUTS - teensy pins for analog inputs
+extern const int analogPins[];
+
+#define NUM_CC_BANKS 5
+#define NUM_CC_POTS 5
+extern int pots[NUM_CC_BANKS][NUM_CC_POTS]; // the MIDI CC (continuous controller) for each analog input
+
+using Micros = unsigned long; // for tracking time per pattern
+
+
+struct SysSettings
+{
+ OMXMode omxMode = DEFAULT_MODE;
+ OMXMode newmode = DEFAULT_MODE;
+ uint8_t midiChannel = 0;
+ int playingPattern;
+ bool refresh = false;
+ bool screenSaverMode = false;
+ unsigned long timeElasped;
+};
+
+extern SysSettings sysSettings;
+
+extern const int potCount;
+
+struct PotSettings
+{
+ ResponsiveAnalogRead *analog[NUM_CC_POTS];
+
+ // ANALOGS
+ int potbank = 0;
+ int analogValues[NUM_CC_POTS] = {0, 0, 0, 0, 0}; // default values
+ int potValues[NUM_CC_POTS] = {0, 0, 0, 0, 0};
+ int hiResPotVal[NUM_CC_POTS] = {0, 0, 0, 0, 0};
+ int potCC = pots[potbank][0];
+ int potVal = analogValues[0];
+ int potNum = 0;
+};
+// Put in global struct to share across classes
+extern PotSettings potSettings;
+
+extern int potMinVal;
+extern int potMaxVal;
+
+
+struct MidiConfig
+{
+ uint8_t defaultVelocity = 100;
+ int octave = 0; // default C4 is 0 - range is -4 to +5
+ // int newoctave = octave;
+ int transpose = 0;
+ int rotationAmt = 0;
+
+ uint8_t swing = 0;
+ const int maxswing = 100;
+ // int swing_values[maxswing] = {0, 1, 3, 5, 52, 66, 70, 72, 80, 99 }; // 0 = off, <50 early swing , >50 late swing, 99=drunken swing
+
+ bool keyState[27] = {false};
+ int midiKeyState[27] = {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1};
+ int midiChannelState[27] = {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1};
+ int rrChannel = 0;
+ bool midiRoundRobin = false;
+ int midiRRChannelOffset = 0;
+ int midiRRChannelCount = 1;
+ uint8_t midiLastNote = 0;
+ uint8_t midiLastVel = 0;
+ int currpgm = 0;
+ int currbank = 0;
+ bool midiInToCV = true;
+ bool midiSoftThru = false;
+ bool midiAUX = false;
+ bool isBankSelect = false;
+};
+
+extern MidiConfig midiSettings;
+
+// struct MidiPage {
+// const int numPages = 4;
+// const int numParams = numPages * 5;
+// int miparam = 0; // midi params item counter
+// int mmpage = 0;
+// };
+// extern MidiPage midiPageParams;
+
+struct MidiMacroConfig
+{
+ int midiMacro = 0;
+ bool m8AUX = false;
+ int midiMacroChan = 10;
+};
+
+extern MidiMacroConfig midiMacroConfig;
+
+// extern bool m8mutesolo[];
+
+struct EncoderConfig
+{
+ bool enc_edit = false;
+};
+
+extern EncoderConfig encoderConfig;
+
+extern const uint32_t secs2micros;
+
+struct ClockConfig
+{
+ float clockbpm = 120;
+ uint8_t globalQuantizeStepIndex = 9; // Determines what unit to quantize to. Index of kArpRates
+ float newtempo = clockbpm;
+ unsigned long tempoStartTime;
+ unsigned long tempoEndTime;
+ float step_delay; // 16th note step length in milliseconds
+ unsigned long minDelta = 5000;
+
+ volatile unsigned long step_micros; // 16th note step in microseconds (quarter of quarter note), 124992 for 120 bpm : 35712 for 300 bpm
+ volatile unsigned long ppqInterval; // time in microseconds between clock ticks, 5208 or 5.2ms for 120 bpm : 1488 for 300 bpm, 5.2 * 96 = 500ms
+ bool send_always = true;
+};
+
+extern ClockConfig clockConfig;
+
+struct SequencerConfig
+{
+ int selectedStep = 0;
+ int selectedNote = 0;
+
+ bool plockDirty[NUM_CC_POTS] = {false, false, false, false, false};
+ int prevPlock[NUM_CC_POTS] = {0, 0, 0, 0, 0};
+
+ volatile unsigned long noteon_micros;
+ volatile unsigned long noteoff_micros;
+
+ uint32_t currentFrameMicros;
+ uint32_t lastClockMicros;
+
+ uint8_t midiOutClockTick; // Shouldn't be modified
+
+ uint16_t currentClockTick; // Counter that wraps from 0-96 on the clock tick. currentClockTick % 96 will align with global 1/4 note, currentClockTick % 96/2=48 global 8th note and 96/4=24 global 16th note
+
+ int numOfActiveArps = 0;
+
+ // bool noteSelect = false;
+ // bool noteSelection = false;
+
+ // int omxSeqSelectedStep = 0;
+ // bool stepSelect = false;
+ // bool stepRecord = false;
+ // bool stepDirty = false;
+};
+extern SequencerConfig seqConfig;
+
+// struct SequencerPage {
+// bool patternParams = false;
+// bool seqPages = false;
+
+// int nspage = 0;
+// int pppage = 0;
+// int sqpage = 0;
+// int srpage = 0;
+
+// int nsparam = 0; // note select params
+// int ppparam = 0; // pattern params
+// int sqparam = 0; // seq params
+// int srparam = 0; // step record params
+// };
+// extern SequencerPage seqPageParams;
+
+struct ColorConfig
+{
+ uint32_t screensaverColor = 0xFF0000;
+ uint32_t stepColor = 0x000000;
+ uint32_t muteColor = 0x000000;
+ uint16_t midiBg_Hue = 0;
+ uint8_t midiBg_Sat = 255;
+ uint8_t midiBg_Brightness = 255;
+
+ uint32_t selMidiFXGRPOffColor = SALMON; // Color of FX Group key when selected
+ uint32_t midiFXGRPOffColor = RED; // Color of FX Group key to turn off MidiFX
+ uint32_t selMidiFXGRPColor = LTCYAN;
+ uint32_t midiFXGRPColor = BLUE;
+ uint32_t midiFXEmptyColor = 0x080808;
+
+ uint32_t arpOn = MINT;
+ uint32_t arpOff = DKCYAN;
+ uint32_t arpHoldOn = YELLOW;
+ uint32_t arpHoldOff = DKYELLOW;
+
+ uint32_t gotoArpParams = DKORANGE;
+ uint32_t nextArpPattern = DKPURPLE;
+ uint32_t nextArpOctave = DKPURPLE;
+
+ uint32_t octDnColor = ORANGE;
+ uint32_t octUpColor = RBLUE;
+
+ uint32_t mfxQuickEdit = RED;
+
+ uint32_t mfxNone = LEDOFF;
+ uint32_t mfxChance = MEDRED;
+ uint32_t mfxTranspose = PURPLE;
+ uint32_t mfxRandomizer = RED;
+ uint32_t mfxSelector = ORANGE;
+ uint32_t mfxChord = CYAN;
+ uint32_t mfxHarmonizer = ROSE;
+ uint32_t mfxScaler = YELLOW;
+ uint32_t mfxMonophonic = INDIGO;
+ uint32_t mfxRepeat = RED;
+ uint32_t mfxArp = BLUE;
+
+ uint32_t getMidiFXColor(uint8_t mfxType)
+ {
+ switch (mfxType)
+ {
+ case MIDIFX_NONE:
+ return mfxNone;
+ case MIDIFX_CHANCE:
+ return mfxChance;
+ case MIDIFX_TRANSPOSE:
+ return mfxTranspose;
+ case MIDIFX_RANDOMIZER:
+ return mfxRandomizer;
+ case MIDIFX_SELECTOR:
+ return mfxSelector;
+ case MIDIFX_CHORD:
+ return mfxChord;
+ case MIDIFX_HARMONIZER:
+ return mfxHarmonizer;
+ case MIDIFX_SCALER:
+ return mfxScaler;
+ case MIDIFX_MONOPHONIC:
+ return mfxMonophonic;
+ case MIDIFX_REPEAT:
+ return mfxRepeat;
+ case MIDIFX_ARP:
+ return mfxArp;
+ };
+
+ return LEDOFF;
+ }
+};
+
+extern ColorConfig colorConfig;
+
+struct ScaleConfig
+{
+ int scaleRoot = 0;
+ int scalePattern = -1;
+ bool lockScale = false; // If Scale is locked you will be unable to play notes out of the scale.
+ bool lockedState = false; // for holding previous scale lock state
+ bool group16 = false; // If group16 is active, all notes in scale will be grouped into lower 16 notes.
+ bool groupedState = false; // for holding previous group16 state
+ bool scaleSelectHold;
+ bool showScaleInSeq = false;
+};
+
+extern ScaleConfig scaleConfig;
+
+struct MidiNoteGroup
+{
+ uint8_t channel = 1;
+ uint8_t noteNumber = 0;
+ // uint8_t keyIndex = 0; // use if t
+ uint8_t prevNoteNumber = 0; // note number before being modified by midiFX
+ uint8_t velocity = 100;
+ float stepLength = 0; // fraction or multiplier of clockConfig.step_micros, 1 == 1 step
+ bool sendMidi = true;
+ bool sendCV = true;
+ uint32_t noteonMicros = 0;
+ bool unknownLength = false;
+ bool noteOff = false; // Set true if note off, corresponding note on should have stepLength of 0
+};
+
+#define NUM_DISP_PARAMS 5
+
+extern const float kNoteLengths[];
+extern const uint8_t kNumNoteLengths;
+
+extern const uint8_t kArpRates[];
+extern const uint8_t kNumArpRates;
+
+extern const int gridh;
+extern const int gridw;
+extern const int PPQ;
+
+extern const char *mfxOffMsg;
+extern const char *mfxArpEditMsg;
+extern const char *mfxPassthroughEditMsg;
+extern const char *exitMsg;
+extern const char *paramOffMsg;
+extern const char *paramOnMsg;
+
+extern const char *modes[];
+extern const char *macromodes[];
+extern const int nummacromodes;
+extern const char *infoDialogText[];
+
+extern String tempString;
+extern String tempStrings[];
+
+enum multDiv
+{
+ MD_QUART = 0,
+ MD_HALF,
+ MD_ONE,
+ MD_TWO,
+ MD_FOUR,
+ MD_EIGHT,
+ MD_SIXTEEN,
+
+ NUM_MULTDIVS
+};
+
+extern float multValues[];
+extern const char *mdivs[];
+
+enum Dialogs
+{
+ COPY = 0,
+ PASTE,
+ CLEAR,
+ RESET,
+ FWD,
+ REV,
+ SAVED,
+ SAVE,
+
+ NUM_DIALOGS
+};
+struct InfoDialogs
+{
+ const char *text;
+ bool state;
+};
+
+extern InfoDialogs infoDialog[NUM_DIALOGS];
+
+enum SubModes
+{
+ SUBMODE_MIDI = 0,
+ SUBMODE_SEQ,
+ SUBMODE_SEQ2,
+ SUBMODE_NOTESEL,
+ SUBMODE_NOTESEL2,
+ SUBMODE_NOTESEL3,
+ SUBMODE_PATTPARAMS,
+ SUBMODE_PATTPARAMS2,
+ SUBMODE_PATTPARAMS3,
+ SUBMODE_STEPREC,
+ SUBMODE_MIDI2,
+ SUBMODE_MIDI3,
+ SUBMODE_MIDI4,
+
+ SUBMODES_COUNT
+};
+
+// KEY SWITCH ROWS/COLS
+#define ROWS 5 // five rows
+#define COLS 6 // six columns
+
+// Map the keys
+extern char keys[ROWS][COLS];
+extern byte rowPins[ROWS]; // row pins for key switches
+extern byte colPins[COLS]; // column pins for key switches
+
+// KEYBOARD MIDI NOTE LAYOUT
+extern const int notes[];
+extern const int steps[];
+extern const int midiKeyMap[];
+
+#endif
diff --git a/Archive/OMX-27-firmware/src/consts/colors.h b/Archive/OMX-27-firmware/src/consts/colors.h
new file mode 100644
index 00000000..0fb5eb6a
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/consts/colors.h
@@ -0,0 +1,91 @@
+#pragma once
+
+#include
+
+// COLOR PRESETS
+// https://www.rapidtables.com/web/color/color-wheel.html
+// hsl(xxx, 100%, 50%)
+const auto RED = 0xFF0000;
+const auto ORANGE = 0xFF8000;
+const auto YELLOW = 0xFFFF00;
+const auto LIME = 0x80FF00;
+const auto GREEN = 0x00FF00;
+const auto MINT = 0x00FF80;
+const auto CYAN = 0x00FFFF;
+const auto RBLUE = 0x007FFF;
+const auto BLUE = 0x0000FF;
+const auto PURPLE = 0x7F00FF;
+const auto MAGENTA = 0xFF00FF;
+const auto ROSE = 0xFF0080;
+
+// hsl(xxx, 50%, 50%)
+const auto MEDRED = 0xBF4040;
+const auto MEDBLUE = 0x4040BF;
+const auto MEDYELLOW = 0xBFBF40;
+
+// hsl(xxx, 100%, 75%)
+const auto LTCYAN = 0x80FFFF;
+const auto LTPURPLE = 0xBF80FF;
+const auto SALMON = 0xFF8080;
+const auto PINK = 0xFF80D4;
+const auto LTYELLOW = 0xFFFF80;
+
+// hsl(xxx, 100%, 15%)
+const auto DKRED = 0x800000;
+const auto DKORANGE = 0x4D2600;
+const auto DKYELLOW = 0x4C4D00;
+const auto DKLIME = 0x408000;
+const auto DKGREEN = 0x264D00;
+const auto DKCYAN = 0x004C4D;
+const auto DKBLUE = 0x00004D;
+const auto DKPURPLE = 0x26004D;
+const auto DKMAGENTA = 0x4D004C;
+const auto INDIGO = 0x4B0082;
+
+// hsl(xxx, 50%, 75%)
+const auto LBLUE = 0x9FCFDF;
+const auto VIOLET = 0xDF9FDF;
+
+// hsl(xxx, 25%, 50%)
+const auto DIMORANGE = 0x9F8060;
+const auto DIMYELLOW = 0x9F9F60;
+const auto DIMLIME = 0x809F60;
+const auto DIMGREEN = 0x609F60;
+const auto DIMMAGENTA = 0x9F609F;
+const auto DIMCYAN = 0x609F9F;
+const auto DIMBLUE = 0x60609F;
+const auto DIMPURPLE = 0x7F609F;
+
+// other
+const auto AMBER = 0x999900;
+const auto BEIGE = 0xFFCC33;
+
+// no color
+
+const auto WHITE = 0xFFFFFF;
+const auto HALFWHITE = 0x808080;
+const auto LOWWHITE = 0x202020;
+const auto VLOWWHITE = 0x101010;
+const auto LEDOFF = 0x000000;
+
+// sequencer pattern colors
+const uint32_t seqColors[] = {ORANGE, YELLOW, GREEN, MAGENTA, CYAN, BLUE, LIME, LTPURPLE};
+const uint32_t muteColors[] = {DKORANGE, DKYELLOW, DKGREEN, DKMAGENTA, DKCYAN, DKBLUE, DKLIME, DKPURPLE};
+const uint32_t sequencePageColors[] = {RED, ORANGE, YELLOW, LIME};
+
+const auto MIDINOTEON = WHITE;
+const auto MIDIBG = VLOWWHITE;
+
+const auto SEQCHASE = DKRED;
+const auto SEQMARKER = LOWWHITE;
+const auto SEQSTEP = ORANGE;
+
+const auto NOTESEL = DKCYAN;
+const auto PATTSEL = LIME;
+
+const auto FUNKONE = LTCYAN;
+const auto FUNKTWO = MINT;
+const auto FUNKTHREE = DKBLUE;
+
+const auto SEQ1C = HALFWHITE;
+const auto SEQ2C = DKBLUE;
diff --git a/Archive/OMX-27-firmware/src/consts/consts.h b/Archive/OMX-27-firmware/src/consts/consts.h
new file mode 100644
index 00000000..b53b3c75
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/consts/consts.h
@@ -0,0 +1,58 @@
+#pragma once
+
+// OMX-27 shared constants
+
+// HW_VERSIONS
+
+// AUTOMATICALLY GET BOARD TYPE - DO NOT MODIFY
+#ifdef ARDUINO_TEENSY40
+#define T4 1
+#else
+#define T4 0
+#endif
+
+#define DEV 0
+#define MIDIONLY 0
+
+// Comment out defines to disable modes if needed for debug build
+#define OMXMODEGRIDS
+
+// HARDWARE Pin for CVGATE_PIN = 13 on beta1 boards, 22 on bodge/midi, 23 on 1.0
+#if DEV
+const int CVGATE_PIN = 13;
+#elif T4
+const int CVGATE_PIN = 13;
+#elif MIDIONLY
+const int CVGATE_PIN = 22; // 13 on beta1 boards, A10 (broken) on test/midi, 23 on 1.0
+#else
+const int CVGATE_PIN = 23; // 13 on beta1 boards, 22 on test, 23 on 1.0
+#endif
+
+#if T4
+// const int CVPITCH_PIN = A14;
+#else
+const int CVPITCH_PIN = A14;
+#endif
+
+const int loSkip = 0;
+const int hiSkip = 0;
+constexpr int range = 4096 - loSkip - hiSkip;
+
+const float fullRangeV = 4.66;
+const float fullRangeDAC = 4095.0;
+const float stepsPerVolt = fullRangeDAC / fullRangeV;
+const float stepsPerOctave = stepsPerVolt;
+const float stepsPerSemitone = stepsPerOctave / 12;
+
+const uint8_t midiMiddleC = 60;
+const uint8_t cvLowestNote = midiMiddleC - 3 * 12; // 3 is how many octaves under middle c
+const uint8_t cvHightestNote = cvLowestNote + int(fullRangeV * 12) - 1;
+
+// FONTS
+#define FONT_LABELS u8g2_font_5x8_tf
+#define FONT_VALUES u8g2_font_7x14B_tf
+#define FONT_SYMB u8g2_font_9x15_m_symbols
+#define FONT_SYMB_BIG u8g2_font_cu12_h_symbols
+#define FONT_TENFAT u8g2_font_tenfatguys_tf
+#define FONT_BIG u8g2_font_helvB18_tr
+#define FONT_CHAR16 u8g2_font_6x12_tf
diff --git a/Archive/OMX-27-firmware/src/hardware/omx_disp.cpp b/Archive/OMX-27-firmware/src/hardware/omx_disp.cpp
new file mode 100644
index 00000000..36c6fbfb
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/hardware/omx_disp.cpp
@@ -0,0 +1,1521 @@
+#include
+
+#include "omx_disp.h"
+#include "../consts/consts.h"
+#include "../ClearUI/ClearUI.h"
+
+U8G2_FOR_ADAFRUIT_GFX u8g2_display;
+
+const char *loaderAnim[] = {"\u25f0", "\u25f1", "\u25f2", "\u25f3"};
+
+// Constructor
+OmxDisp::OmxDisp()
+{
+}
+
+
+
+void OmxDisp::setup()
+{
+ initializeDisplay();
+ u8g2_display.begin(display);
+}
+
+void OmxDisp::clearDisplay()
+{
+ // Clear display
+ display.display();
+ setDirty();
+}
+
+void OmxDisp::drawStartupScreen()
+{
+ display.clearDisplay();
+ testdrawrect();
+ delay(200);
+ display.clearDisplay();
+ u8g2_display.setForegroundColor(WHITE);
+ u8g2_display.setBackgroundColor(BLACK);
+ drawLoading();
+ // Display version
+ display.clearDisplay();
+ displayMessageTimed("v" + String(MAJOR_VERSION) + "." + String(MINOR_VERSION) + "." + String(POINT_VERSION), 1);
+ display.display();
+}
+
+void OmxDisp::displayMessage(String msg)
+{
+ displayMessage(msg.c_str());
+}
+
+void OmxDisp::displayMessage(const char *msg)
+{
+ specialMsgType_ = 0;
+ currentMsg = msg;
+
+ display.fillRect(0, 0, 128, 32, BLACK);
+ u8g2_display.setFontMode(1);
+ u8g2_display.setFont(FONT_TENFAT);
+ u8g2_display.setForegroundColor(WHITE);
+ u8g2_display.setBackgroundColor(BLACK);
+ u8g2centerText(msg, 0, 10, 128, 32);
+
+ messageTextTimer = MESSAGE_TIMEOUT_US;
+ dirtyDisplay = true;
+}
+
+void OmxDisp::displayMessagef(const char *fmt, ...)
+{
+ specialMsgType_ = 0;
+ va_list args;
+ va_start(args, fmt);
+ char buf[24];
+ vsnprintf(buf, sizeof(buf), fmt, args);
+ va_end(args);
+ displayMessage(buf);
+}
+
+// Something is keeping weird cache of display names or serial logs in memory
+void OmxDisp::displayMessageTimed(String msg, uint8_t secs)
+{
+ currentMsg = msg;
+ specialMsgType_ = 0;
+
+ renderMessage();
+
+ messageTextTimer = secs * 100000;
+ dirtyDisplay = true;
+}
+
+void OmxDisp::displaySpecialMessage(uint8_t msgType, String msg, uint8_t secs)
+{
+ currentMsg = msg;
+ specialMsgType_ = msgType;
+
+ renderMessage();
+
+ messageTextTimer = secs * 100000;
+ dirtyDisplay = true;
+}
+
+void OmxDisp::chordBalanceMsg(int8_t balArray[], float velArray[], uint8_t secs)
+{
+ for (uint8_t i = 0; i < 4; i++)
+ {
+ chordBalArray_[i] = balArray[i];
+ chordVelArray_[i] = velArray[i];
+ }
+
+ currentMsg = "Balance";
+ specialMsgType_ = 1;
+
+ renderMessage();
+
+ messageTextTimer = secs * 100000;
+ dirtyDisplay = true;
+}
+
+void OmxDisp::renderMessage()
+{
+ if (specialMsgType_ == 0)
+ {
+ display.fillRect(0, 0, 128, 32, BLACK);
+ u8g2_display.setFontMode(1);
+ u8g2_display.setFont(FONT_TENFAT);
+ u8g2_display.setForegroundColor(WHITE);
+ u8g2_display.setBackgroundColor(BLACK);
+ u8g2centerText(currentMsg.c_str(), 0, 10, 128, 32);
+ // dirtyDisplay = true;
+ }
+ else if (specialMsgType_ == 1)
+ {
+ dispChordBalance();
+ }
+}
+
+bool OmxDisp::isMessageActive()
+{
+ return messageTextTimer > 0;
+}
+
+void OmxDisp::u8g2centerText(const char *s, int16_t x, int16_t y, uint16_t w, uint16_t h)
+{
+ // int16_t bx, by;
+ uint16_t bw, bh;
+ bw = u8g2_display.getUTF8Width(s);
+ bh = u8g2_display.getFontAscent();
+ u8g2_display.setCursor(
+ x + (w - bw) / 2,
+ y + (h - bh) / 2);
+ u8g2_display.print(s);
+}
+
+void OmxDisp::u8g2leftText(const char *s, int16_t x, int16_t y, uint16_t w, uint16_t h)
+{
+ uint16_t bh = u8g2_display.getFontAscent();
+ u8g2_display.setCursor(
+ x,
+ y + (h - bh) / 2);
+ u8g2_display.print(s);
+}
+
+void OmxDisp::u8g2centerNumber(int n, uint16_t x, uint16_t y, uint16_t w, uint16_t h)
+{
+ char buf[8];
+ itoa(n, buf, 10);
+ u8g2centerText(buf, x, y, w, h);
+}
+
+void OmxDisp::testdrawrect()
+{
+ display.clearDisplay();
+
+ for (int16_t i = 0; i < display.height() / 2; i += 2)
+ {
+ display.drawRect(i, i, display.width() - 2 * i, display.height() - 2 * i, SSD1306_WHITE);
+ display.display(); // Update screen with each newly-drawn rectangle
+ delay(1);
+ }
+
+ delay(500);
+}
+
+void OmxDisp::drawLoading()
+{
+ display.clearDisplay();
+ u8g2_display.setFontMode(0);
+ for (int16_t i = 0; i < 16; i += 1)
+ {
+ display.clearDisplay();
+ u8g2_display.setCursor(18, 18);
+ u8g2_display.setFont(FONT_TENFAT);
+ u8g2_display.print("OMX-27");
+ u8g2_display.setFont(FONT_SYMB_BIG);
+ u8g2centerText(loaderAnim[i % 4], 80, 10, 32, 32); // "\u00BB\u00AB" // // dice: "\u2685"
+ display.display();
+ delay(100);
+ }
+
+ delay(100);
+}
+
+void OmxDisp::dispGridBoxes()
+{
+ display.fillRect(0, 0, gridw, 10, WHITE);
+ display.drawFastVLine(gridw / 4, 0, gridh, INVERSE);
+ display.drawFastVLine(gridw / 2, 0, gridh, INVERSE);
+ display.drawFastVLine(gridw * 0.75, 0, gridh, INVERSE);
+}
+void OmxDisp::invertColor(bool flip)
+{
+ if (flip)
+ {
+ u8g2_display.setForegroundColor(BLACK);
+ u8g2_display.setBackgroundColor(WHITE);
+ }
+ else
+ {
+ u8g2_display.setForegroundColor(WHITE);
+ u8g2_display.setBackgroundColor(BLACK);
+ }
+}
+void OmxDisp::dispValBox(int v, int16_t n, bool inv)
+{ // n is box 0-3
+ invertColor(inv);
+ u8g2centerNumber(v, n * 32, hline * 2 + 3, 32, 22);
+}
+
+void OmxDisp::dispSymbBox(const char *v, int16_t n, bool inv)
+{ // n is box 0-3
+ invertColor(inv);
+ u8g2centerText(v, n * 32, hline * 2 + 3, 32, 22);
+}
+
+void OmxDisp::clearLegends()
+{
+ legends[0] = "";
+ legends[1] = "";
+ legends[2] = "";
+ legends[3] = "";
+ legendVals[0] = -127;
+ legendVals[1] = -127;
+ legendVals[2] = -127;
+ legendVals[3] = -127;
+ dispPage = 0;
+ legendText[0] = "";
+ legendText[1] = "";
+ legendText[2] = "";
+ legendText[3] = "";
+ useLegendString[0] = false;
+ useLegendString[1] = false;
+ useLegendString[2] = false;
+ useLegendString[3] = false;
+}
+
+bool OmxDisp::validateLegendIndex(uint8_t index)
+{
+ if(index >= 4)
+ {
+ Serial.println("ERROR: Param index out of range!");
+ return false;
+ }
+ return true;
+}
+
+
+
+void OmxDisp::setLegend(uint8_t index, const char *label, int value)
+{
+ if (validateLegendIndex(index))
+ {
+ legends[index] = label;
+ legendVals[index] = value;
+ }
+}
+void OmxDisp::setLegend(uint8_t index, const char *label, bool isOff, int value)
+{
+ if (isOff)
+ {
+ setLegend(index, label, paramOffMsg);
+ }
+ else
+ {
+ setLegend(index, label, value);
+ }
+}
+void OmxDisp::setLegend(uint8_t index, const char *label, const char *text)
+{
+ if (validateLegendIndex(index))
+ {
+ legends[index] = label;
+ legendText[index] = text;
+ }
+}
+void OmxDisp::setLegend(uint8_t index, const char* label, bool isOff, const char* text)
+{
+ if (isOff)
+ {
+ setLegend(index, label, paramOffMsg);
+ }
+ else
+ {
+ setLegend(index, label, text);
+ }
+}
+void OmxDisp::setLegend(uint8_t index, const char *label, String text)
+{
+ if (validateLegendIndex(index))
+ {
+ legends[index] = label;
+ useLegendString[index] = true;
+ legendString[index] = text;
+ }
+}
+void OmxDisp::setLegend(uint8_t index, const char *label, bool isOff, String text)
+{
+ if(isOff)
+ {
+ setLegend(index, label, paramOffMsg);
+ }
+ else
+ {
+ setLegend(index, label, text);
+ }
+}
+void OmxDisp::setLegend(uint8_t index, const char* label, bool value)
+{
+ setLegend(index, label, value ? paramOnMsg : paramOffMsg);
+}
+
+void OmxDisp::dispGenericMode(int selected)
+{
+ if (isMessageActive())
+ {
+ renderMessage();
+ return;
+ }
+
+ // const char* legends[4] = {"","","",""};
+ // int legendVals[4] = {0,0,0,0};
+ // int dispPage = 0;
+ // const char* legendText[4] = {"","","",""};
+ // int displaychan = sysSettings.midiChannel;
+
+ u8g2_display.setFontMode(1);
+ u8g2_display.setFont(FONT_LABELS);
+ u8g2_display.setCursor(0, 0);
+ dispGridBoxes();
+
+ // labels
+ u8g2_display.setForegroundColor(BLACK);
+ u8g2_display.setBackgroundColor(WHITE);
+
+ for (int j = 0; j < 4; j++)
+ {
+ u8g2centerText(legends[j], (j * 32) + 1, hline - 2, 32, 10);
+ }
+
+ // value text formatting
+ u8g2_display.setFontMode(1);
+ u8g2_display.setFont(FONT_VALUES);
+ u8g2_display.setForegroundColor(WHITE);
+ u8g2_display.setBackgroundColor(BLACK);
+
+ switch (selected)
+ {
+ case 1:
+ display.fillRect(0 * 32 + 2, 9, 29, 21, WHITE);
+ break;
+ case 2: //
+ display.fillRect(1 * 32 + 2, 9, 29, 21, WHITE);
+ break;
+ case 3: //
+ display.fillRect(2 * 32 + 2, 9, 29, 21, WHITE);
+ break;
+ case 4: //
+ display.fillRect(3 * 32 + 2, 9, 29, 21, WHITE);
+ break;
+ case 0:
+ default:
+ break;
+ }
+ // ValueBoxes
+ int highlight = false;
+ for (int j = 1; j < NUM_DISP_PARAMS; j++)
+ { // start at 1 to only highlight values 1-4
+
+ if (j == selected)
+ {
+ highlight = true;
+ }
+ else
+ {
+ highlight = false;
+ }
+ if (legendVals[j - 1] == -127)
+ {
+ dispSymbBox(legendText[j - 1], j - 1, highlight);
+ }
+ else
+ {
+ dispValBox(legendVals[j - 1], j - 1, highlight);
+ }
+ }
+ if (dispPage != 0)
+ {
+ for (int k = 0; k < 4; k++)
+ {
+ if (dispPage == k + 1)
+ {
+ dispPageIndicators(k, true);
+ }
+ else
+ {
+ dispPageIndicators(k, false);
+ }
+ }
+ }
+}
+
+void OmxDisp::dispGenericMode2(uint8_t numPages, int8_t selectedPage, int8_t selectedParam, bool encSelActive)
+{
+ if (isMessageActive())
+ {
+ renderMessage();
+ return;
+ }
+
+ u8g2_display.setFontMode(1);
+ u8g2_display.setFont(FONT_LABELS);
+ u8g2_display.setCursor(0, 0);
+ dispGridBoxes();
+
+ // labels
+ u8g2_display.setForegroundColor(BLACK);
+ u8g2_display.setBackgroundColor(WHITE);
+
+ for (int j = 0; j < 4; j++)
+ {
+ u8g2centerText(legends[j], (j * 32) + 1, hline - 2, 32, 10);
+ }
+
+ // value text formatting
+ u8g2_display.setFontMode(1);
+ u8g2_display.setFont(FONT_VALUES);
+ u8g2_display.setForegroundColor(WHITE);
+ u8g2_display.setBackgroundColor(BLACK);
+
+ if (selectedParam >= 0 && selectedParam < 4)
+ {
+ if (encSelActive)
+ {
+ const int8_t bWidth = 1;
+ display.fillRect(selectedParam * 32 + 2, 9, 29, 21, WHITE);
+ display.fillRect(selectedParam * 32 + 2 + bWidth, 9 + bWidth, 29 - (bWidth * 2), 21 - (bWidth * 2), BLACK);
+ }
+ else
+ {
+ display.fillRect(selectedParam * 32 + 2, 9, 29, 21, WHITE);
+ }
+
+ // display.fillRect(selectedParam * 32 + 2, 9, 29, 21, WHITE);
+ }
+
+ // ValueBoxes
+ bool highlight = false;
+ for (int j = 0; j < 4; j++)
+ {
+ highlight = (j == selectedParam && !encSelActive);
+
+ if (useLegendString[j])
+ {
+ dispSymbBox(legendString[j].c_str(), j, highlight);
+ }
+ else if (legendVals[j] == -127)
+ {
+ dispSymbBox(legendText[j], j, highlight);
+ }
+ else
+ {
+ dispValBox(legendVals[j], j, highlight);
+ }
+ }
+
+ dispPageIndicators2(numPages, selectedPage);
+}
+
+void OmxDisp::dispGenericModeLabel(const char *label, uint8_t numPages, int8_t selectedPage)
+{
+ if (isMessageActive())
+ {
+ renderMessage();
+ return;
+ }
+
+ display.fillRect(0, 0, 128, 32, BLACK);
+ u8g2_display.setFontMode(1);
+ u8g2_display.setFont(FONT_TENFAT);
+ u8g2_display.setForegroundColor(WHITE);
+ u8g2_display.setBackgroundColor(BLACK);
+ u8g2centerText(label, 0, 10, 128, 32);
+
+ if (numPages > 1)
+ {
+ dispPageIndicators2(numPages, selectedPage);
+ }
+}
+
+void OmxDisp::dispGenericModeLabelDoubleLine(const char *label1, const char *label2, uint8_t numPages, int8_t selectedPage)
+{
+ if (isMessageActive())
+ {
+ renderMessage();
+ return;
+ }
+
+ display.fillRect(0, 0, 128, 32, BLACK);
+ u8g2_display.setFontMode(1);
+ u8g2_display.setFont(FONT_VALUES);
+ u8g2_display.setForegroundColor(WHITE);
+ u8g2_display.setBackgroundColor(BLACK);
+ u8g2centerText(label1, 0, 12, 128, 8);
+ u8g2centerText(label2, 0, 26, 128, 8);
+
+ if (numPages > 1)
+ {
+ dispPageIndicators2(numPages, selectedPage);
+ }
+}
+
+void OmxDisp::dispGenericModeLabelSmallText(const char *label, uint8_t numPages, int8_t selectedPage)
+{
+ if (isMessageActive())
+ {
+ renderMessage();
+ return;
+ }
+
+ display.fillRect(0, 0, 128, 32, BLACK);
+ u8g2_display.setFontMode(1);
+ u8g2_display.setFont(FONT_LABELS);
+ u8g2_display.setForegroundColor(WHITE);
+ u8g2_display.setBackgroundColor(BLACK);
+ u8g2centerText(label, 0, 10, 128, 8);
+
+ if (numPages > 1)
+ {
+ dispPageIndicators2(numPages, selectedPage);
+ }
+}
+
+void OmxDisp::dispOptionCombo(const char * header, const char *optionsArray[], uint8_t optionCount, uint8_t selected, bool encSelActive)
+{
+ if (isMessageActive())
+ {
+ renderMessage();
+ return;
+ }
+
+ // Draw header text
+ u8g2_display.setFontMode(1);
+ u8g2_display.setFont(FONT_LABELS);
+ u8g2_display.setCursor(0, 0);
+
+ uint8_t labelWidth = 128; // 8
+ u8g2centerText(header, 2, hline - 2, labelWidth - 4, 10);
+
+ // Draw options
+ u8g2_display.setFontMode(1);
+ u8g2_display.setFont(FONT_LABELS);
+
+ uint8_t optionWidth = 128 / optionCount; // 8
+
+ uint8_t yPos = hline * 2 + 3; // 19
+
+ for (uint8_t i = 0; i < optionCount; i++)
+ {
+ if (i == selected)
+ {
+ display.fillRect(i * optionWidth, 14, optionWidth, 12, WHITE);
+ display.fillRect(i * optionWidth + 1, 14 + 1, optionWidth - 2, 12 - 2, BLACK);
+ }
+
+ u8g2centerText(optionsArray[i], i * optionWidth, yPos, optionWidth - 1, 16);
+ }
+
+ if(!encSelActive)
+ {
+ display.drawFastHLine(4, 28, 128 - 8, WHITE);
+ }
+}
+
+void OmxDisp::dispChar16(const char *charArray[], uint8_t charCount, uint8_t selected, uint8_t numPages, int8_t selectedPage, bool encSelActive, bool showLabels, const char *labels[], uint8_t labelCount)
+{
+ if (isMessageActive())
+ {
+ renderMessage();
+ return;
+ }
+
+ display.fillRect(0, 0, 128, 32, BLACK);
+
+ if (showLabels)
+ {
+ int8_t selIndex = constrain(selected - 16, -1, 127);
+ dispLabelParams(selIndex, encSelActive, labels, labelCount, false);
+ }
+
+ uint8_t charWidth = 128 / 16; // 8
+
+ u8g2_display.setFontMode(1);
+ u8g2_display.setFont(FONT_CHAR16);
+
+ uint8_t yPos = hline * 2 + 3; // 19
+
+ for (uint8_t i = 0; i < 16; i++)
+ {
+ bool showChar = i < charCount;
+ if (i == selected)
+ {
+ display.drawFastHLine(i * charWidth + 1, 26, charWidth - 2, WHITE);
+
+ if (encSelActive == false)
+ {
+ display.fillRect(i * charWidth, 14, charWidth, 10, WHITE);
+ invertColor(true);
+ showChar = true;
+ }
+ else
+ {
+ invertColor(false);
+ }
+ // display.drawLine(i * charWidth - charWidth + 1, yPos + 2, i * charWidth - 1, yPos + 2, WHITE);
+ }
+ else
+ {
+ invertColor(false);
+ }
+
+ if (showChar)
+ {
+ u8g2centerText(charArray[i], i * charWidth, yPos, charWidth - 1, 16);
+ }
+ }
+
+ // if (numPages > 1)
+ // {
+ // dispPageIndicators2(numPages, selectedPage);
+ // }
+}
+
+void OmxDisp::dispValues16(int8_t valueArray[], uint8_t valueCount, int8_t minValue, int8_t maxValue, bool centered, uint8_t selected, uint8_t numPages, int8_t selectedPage, bool encSelActive, bool showLabels, const char *labels[], uint8_t labelCount)
+{
+ if (isMessageActive())
+ {
+ renderMessage();
+ return;
+ }
+
+ display.fillRect(0, 0, 128, 32, BLACK);
+
+ if (showLabels)
+ {
+ int8_t selIndex = constrain(selected - 16, -1, 127);
+ dispLabelParams(selIndex, encSelActive, labels, labelCount, false);
+ }
+
+ uint8_t boxWidth = 128 / 16; // 8
+ uint8_t boxStartY = 10;
+ uint8_t heightMax = 32;
+ uint8_t boxHeight = heightMax - boxStartY;
+ uint8_t halfBoxHeight = boxHeight / 2;
+
+ int8_t middleValue = ((maxValue - minValue) / 2) + minValue;
+
+ for (uint8_t i = 0; i < 16; i++)
+ {
+ if (i < valueCount && valueArray[i] == -127)
+ continue;
+
+ uint16_t fgColor = WHITE;
+
+ uint8_t xPos = i * boxWidth + 2;
+ uint8_t width = boxWidth - 4;
+
+ if (i == selected && encSelActive)
+ {
+ display.fillRect(i * boxWidth, boxStartY, boxWidth, boxHeight, WHITE);
+ display.fillRect(i * boxWidth + 1, boxStartY + 1, boxWidth - 2, boxHeight - 2, BLACK);
+ }
+
+ if (i >= valueCount)
+ {
+ // display.fillRect(i * boxWidth + 3, boxStartY + (halfBoxHeight + 1), 1, 1, fgColor);
+
+ continue;
+ }
+
+ if (centered)
+ {
+ if (valueArray[i] >= middleValue)
+ {
+ float valuePerc = constrain(map((float)valueArray[i], (float)middleValue, (float)maxValue, 0.0f, 1.0f), 0.0f, 1.0f);
+ uint8_t valueHeight = max(halfBoxHeight * valuePerc, 0);
+ display.fillRect(xPos, boxStartY + (halfBoxHeight + 1) - valueHeight, width, valueHeight + 1, fgColor);
+
+ // if(i == selected)
+ // {
+ // Serial.println("valuePerc: " + String(valuePerc) + " valueHeight: " + String(valueHeight) + " startY: " + String(boxStartY + halfBoxHeight - valueHeight));
+ // }
+ }
+ else
+ {
+ float valuePerc = 1.0f - constrain(map((float)valueArray[i], (float)minValue, (float)middleValue, 0.0f, 1.0f), 0.0f, 1.0f);
+ uint8_t valueHeight = constrain((boxHeight - halfBoxHeight) * valuePerc, 0, halfBoxHeight - 3);
+ display.fillRect(xPos, boxStartY + halfBoxHeight + 1, width, valueHeight + 1, fgColor);
+ // display.fillRect(i + 3, boxStartY + halfBoxHeight + 1, boxWidth - 4, valueHeight - 2, bgColor);
+ }
+ }
+ else
+ {
+ float valuePerc = constrain(map((float)valueArray[i], (float)minValue, (float)maxValue, 0.0f, 1.0f), 0.0f, 1.0f);
+ uint8_t valueHeight = constrain(boxHeight * valuePerc, 0, boxHeight - 1);
+ display.fillRect(xPos, boxStartY + boxHeight - valueHeight, width, valueHeight + 1, fgColor);
+ }
+ }
+
+ // if (numPages > 1)
+ // {
+ // dispPageIndicators2(numPages, selectedPage);
+ // }
+}
+
+void OmxDisp::dispParamBar(int8_t potValue, int8_t targetValue, int8_t minValue, int8_t maxValue, bool pickedUp, bool centered, const char* bankName, const char* paramName)
+{
+ if (isMessageActive())
+ {
+ renderMessage();
+ return;
+ }
+
+ display.fillRect(0, 0, 128, 32, BLACK);
+
+ // if (showLabels)
+ // {
+ // int8_t selIndex = constrain(selected - 16, -1, 127);
+ // dispLabelParams(selIndex, encSelActive, labels, labelCount, false);
+ // }
+
+ u8g2_display.setFontMode(1);
+ // u8g2_display.setFont(FONT_LABELS);
+ u8g2_display.setCursor(0, 0);
+
+ u8g2_display.setFont(FONT_LABELS);
+ u8g2leftText(bankName, 2, hline - 3, 128 - 4, 10);
+ u8g2_display.setFont(FONT_TENFAT);
+ u8g2leftText(paramName, 2, 18, 92 - 4, 12);
+
+ u8g2_display.setFont(FONT_BIG);
+ tempString = String(targetValue);
+ u8g2centerText(tempString.c_str(), 92, 18, 128 - 96 - 4, 22);
+
+ float potPerc = map(potValue, minValue, maxValue, 0, 1000) / 1000.0f;
+ float targetPerc = map(targetValue, minValue, maxValue, 0, 1000) / 1000.0f;
+
+ uint8_t boxStartX = 0; // 8
+ uint8_t boxWidth = 128; // 8
+ uint8_t potWidth = boxWidth * potPerc; // 8
+ uint8_t targetWidth = boxWidth * targetPerc; // 8
+ uint8_t boxStartY = 27;
+ uint8_t heightMax = 32;
+ uint8_t boxHeight = heightMax - boxStartY;
+ // uint8_t halfBoxHeight = boxHeight / 2;
+
+ // int8_t middleValue = ((maxValue - minValue) / 2) + minValue;
+
+ display.fillRect(boxStartX, boxStartY, boxWidth, boxHeight, WHITE);
+ display.fillRect(boxStartX + 1, boxStartY + 1, boxWidth - 2, boxHeight - 2, BLACK);
+ display.fillRect(boxStartX + 1, boxStartY + 1, targetWidth - 2, boxHeight - 2, WHITE);
+
+ // Chevron showing pot value
+ // xxxxxxx
+ // -xxxxx
+ // --xxx
+ // ---x
+ display.fillRect(boxStartX + potWidth - 4, boxStartY - 4, 7, 1, WHITE);
+ display.fillRect(boxStartX + potWidth - 3, boxStartY - 3, 5, 1, WHITE);
+ display.fillRect(boxStartX + potWidth - 2, boxStartY - 2, 3, 1, WHITE);
+ display.fillRect(boxStartX + potWidth - 1, boxStartY - 1, 1, 1, WHITE);
+
+ if(!pickedUp)
+ {
+ display.fillRect(boxStartX + potWidth - 3, boxStartY - 4, 5, 1, BLACK);
+ display.fillRect(boxStartX + potWidth - 2, boxStartY - 3, 3, 1, BLACK);
+ display.fillRect(boxStartX + potWidth - 1, boxStartY - 2, 1, 1, BLACK);
+ }
+}
+
+void OmxDisp::dispSlots(const char *slotNames[], uint8_t slotCount, uint8_t selected, uint8_t animPos, bool encSelActive, bool showLabels, const char *labels[], uint8_t labelCount)
+{
+ // if (isMessageActive())
+ // {
+ // renderMessage();
+ // return;
+ // }
+
+ display.fillRect(0, 0, 128, 32, BLACK);
+
+ // if(showLabels)
+ // {
+ // int8_t selIndex = constrain(selected - 16, -1, 127);
+ // dispLabelParams(selIndex, encSelActive, labels, labelCount);
+ // }
+
+ // uint8_t rowCount = slotCount - 1;// Selected slot will be raised
+
+ uint8_t rowCount = 4; // Selected slot will be raised
+
+ int8_t selYOffset = 0; // 14 to 0
+ int8_t horzOffset = 18; // 18 to 1, can reduce after selYOffset <= 1
+
+ if (animPos < 14)
+ {
+ selYOffset = 14 - animPos;
+ }
+
+ if (selYOffset <= 0)
+ {
+ horzOffset = map(constrain(animPos, 13, 26), 13, 26, 18, 2);
+ }
+
+ uint8_t slotWidth = 128 / rowCount;
+ uint8_t slotHeight = 12;
+ uint8_t slotPad = 1;
+
+ // uint8_t charWidth = 128 / 16; // 8
+
+ u8g2_display.setFontMode(1);
+ u8g2_display.setFont(FONT_LABELS);
+
+ // uint8_t yPos = hline * 2 + 3; // 19
+
+ uint8_t yPos = 15; // 19
+
+ int8_t slotIndex = selected - 2;
+ uint8_t slotOffset = 0;
+
+ if (selected == 0)
+ {
+ slotOffset = 2;
+ }
+ else if (selected == 1)
+ {
+ slotOffset = 1;
+ }
+
+ for (int8_t i = slotIndex; i < slotCount; i++)
+ {
+ if (i != selected)
+ {
+ if (slotIndex >= 0 && slotIndex < slotCount)
+ {
+ int8_t hOff = slotOffset < 2 ? -horzOffset + 1 : horzOffset - 2;
+
+ display.fillRect(slotOffset * slotWidth + slotPad + 1 + hOff, yPos, slotWidth - (slotPad * 2) - 2, slotHeight, WHITE);
+ display.fillRect(slotOffset * slotWidth + slotPad + 2 + hOff, yPos + 1, slotWidth - 4 - (slotPad * 2), slotHeight - 2, BLACK);
+ invertColor(false);
+ u8g2centerText(slotNames[i], slotOffset * slotWidth + slotPad + 2 + hOff, yPos + (slotHeight / 2) + 2, slotWidth - 4 - (slotPad * 2), 8);
+ slotOffset++;
+ }
+ slotIndex++;
+
+ if (slotOffset >= 4)
+ {
+ break;
+ }
+ }
+ }
+
+ // Display selected slot
+ slotWidth = 36;
+ slotHeight = 13;
+ yPos = 0 + selYOffset; // 19
+ uint8_t selectedStart = 64 - (slotWidth / 2);
+
+ display.fillRect(selectedStart + slotPad, yPos, slotWidth - (slotPad * 2), slotHeight, WHITE);
+ display.fillRect(selectedStart + slotPad + 1, yPos + 1, slotWidth - 2 - (slotPad * 2), slotHeight - 2, BLACK);
+ invertColor(false);
+ u8g2_display.setFont(FONT_CHAR16);
+ u8g2centerText(slotNames[selected], selectedStart + slotPad + 1, yPos + (slotHeight / 2) + 3, slotWidth - 2 - (slotPad * 2), 8);
+
+ if (yPos + slotHeight < 25)
+ {
+ display.drawLine(63, yPos + slotHeight, 63, 25, WHITE);
+ }
+}
+
+void OmxDisp::dispCenteredSlots(const char *slotNames[], uint8_t slotCount, uint8_t selected, bool encoderSelect, bool showLabels, bool centerLabels, const char *labels[], uint8_t labelCount)
+{
+ if (isMessageActive())
+ {
+ renderMessage();
+ return;
+ }
+
+ display.fillRect(0, 0, 128, 32, BLACK);
+
+ uint8_t slotWidth = 128 / slotCount;
+
+ for (uint8_t i = 0; i < slotCount; i++)
+ {
+ dispParamLabel(i * slotWidth, 10, slotWidth, 18, selected == i, 1, encoderSelect, true, slotNames[i], FONT_VALUES, 1, true);
+ }
+
+ // dispParamLabel(32, 10, 32, 18, selected == 1, 1, encoderSelect, true, octaveName, FONT_VALUES, 1, true);
+ // dispParamLabel(0, 0, 128, 10, selected == 3, 0, encoderSelect, true, chordType, FONT_LABELS, 1, true);
+
+ if (showLabels)
+ {
+ int8_t selIndex = constrain(selected - slotCount, -1, 127);
+ dispLabelParams(selIndex, encoderSelect, labels, labelCount, centerLabels);
+ }
+}
+
+void OmxDisp::dispKeyboard(int rootNote, int noteNumbers[], bool showLabels, const char *labels[], uint8_t labelCount)
+{
+ if (isMessageActive())
+ {
+ renderMessage();
+ return;
+ }
+
+ const uint8_t wkWidth = 7;
+ const uint8_t wkInc = 6;
+
+ const uint8_t wkHeight = 22;
+ const uint8_t wkStartX = 16;
+ const uint8_t wkStartY = 10;
+
+ const uint8_t bkWidth = 7;
+ const uint8_t bkInc = 6;
+
+ const uint8_t bkHeight = 16;
+ const uint8_t bkStartX = 13;
+ const uint8_t bkStartY = 9;
+
+ display.fillRect(0, 0, 128, 32, BLACK);
+
+ // Find and split up black and white notes
+ bool blackNotes[10];
+ bool whiteNotes[16];
+
+ for (uint8_t i = 0; i < 16; i++)
+ {
+ if (i < 10)
+ {
+ blackNotes[i] = false;
+ }
+ whiteNotes[i] = false;
+ }
+
+ // int rootNote = -1;
+
+ // // Find the lowest note
+ // for(uint8_t i = 0; i < 6; i++)
+ // {
+ // if(noteNumbers[i] >= 0 && noteNumbers[i] <= 127)
+ // {
+ // if(rootNote < 0 || noteNumbers[i] < rootNote)
+ // {
+ // rootNote = noteNumbers[i];
+ // }
+ // }
+ // }
+
+ bool addOctave = rootNote % 24 >= 12;
+
+ for (uint8_t i = 0; i < 6; i++)
+ {
+ int note = noteNumbers[i];
+
+ // If valid note
+ if (note >= 0 && note <= 127)
+ {
+ // uint8_t threeOctNote = (note + (addOctave ? 12 : 0)) % 36;
+
+ // C edge case if note is 2 octaves above root since there's
+ // one extra C
+ if (note - rootNote == 24)
+ {
+ whiteNotes[15] = true;
+ continue;
+ }
+
+ uint8_t twoOctNote = (note + (addOctave ? 12 : 0)) % 24;
+
+ for (uint8_t j = 1; j < 27; j++)
+ {
+ uint8_t stepNote = (notes[j] + 12) % 24; // Turn note lookup into 0-24 semitones
+
+ // B edge case
+ if (j == 11)
+ {
+ // If note is b and less than root note
+ if (note % 12 == 11 && note < rootNote)
+ {
+ whiteNotes[j - 11] = true;
+ break;
+ }
+ }
+
+ if (twoOctNote == stepNote)
+ {
+ if (j >= 11)
+ {
+ whiteNotes[j - 11] = true;
+ }
+ else
+ {
+ blackNotes[j - 1] = true;
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ // draw white keys
+ for (uint8_t i = 0; i < 16; i++)
+ {
+ if (whiteNotes[i] == false)
+ {
+ // display.fillRect(startX + (wkWidth * i), wkStartY, wkWidth, wkHeight, WHITE);
+ display.drawRect(wkStartX + (wkInc * i), wkStartY, wkWidth, wkHeight, WHITE);
+ }
+ }
+
+ for (uint8_t i = 0; i < 16; i++)
+ {
+ if (whiteNotes[i])
+ {
+ display.drawRect(wkStartX + (wkInc * i), wkStartY, wkWidth, wkHeight, BLACK);
+ display.fillRect(wkStartX + (wkInc * i) + 1, wkStartY, wkWidth - 2, wkHeight, WHITE);
+ }
+ }
+
+ uint8_t bOffset = 0;
+
+ // draw black keys
+ // Two additional keys for sides
+ for (uint8_t i = 0; i < 12; i++)
+ {
+ bool blackOn = false;
+
+ if (i == 1 || i == 3 || i == 6 || i == 8 || i == 11)
+ {
+ bOffset += 6;
+ }
+
+ uint8_t xStart = bkStartX + bOffset + (bkInc * i);
+
+ if (i > 0 && i < 11)
+ {
+ blackOn = blackNotes[i - 1];
+ }
+ else
+ {
+ display.fillRect(xStart, bkStartY, bkWidth, bkHeight, BLACK);
+ display.drawRect(xStart + 1, bkStartY + 1, bkWidth - 2, bkHeight - 2, WHITE);
+ display.fillRect(xStart + 2, bkStartY, bkWidth - 4, bkHeight - 1, BLACK);
+ continue;
+ ;
+ }
+
+ if (blackOn)
+ {
+ display.fillRect(xStart, bkStartY, bkWidth, bkHeight, BLACK);
+ display.fillRect(xStart + 1, bkStartY + 1, bkWidth - 2, bkHeight - 2, WHITE);
+ }
+ else
+ {
+ // display.fillRect(startX + (wkWidth * i), wkStartY, wkWidth, wkHeight, WHITE);
+ display.fillRect(xStart, bkStartY, bkWidth, bkHeight, BLACK);
+ display.drawRect(xStart + 1, bkStartY + 1, bkWidth - 2, bkHeight - 2, WHITE);
+ }
+ }
+
+ display.fillRect(0, 10, 16, 32, BLACK); // trim left side
+ display.fillRect(113, 10, 15, 32, BLACK); // trim right side
+ display.drawLine(18, 10, 110, 10, WHITE); // Cap the top
+
+ if (!whiteNotes[0])
+ {
+ display.drawLine(16, 24, 16, 31, WHITE); // Left wall
+ }
+
+ if (!whiteNotes[15])
+ {
+ display.drawLine(112, 24, 112, 31, WHITE); // Right wall
+ }
+
+ if (showLabels)
+ {
+ // int8_t selIndex = constrain(selected - 16, -1, 127);
+ dispLabelParams(-1, true, labels, labelCount, true);
+ }
+}
+
+void OmxDisp::dispChordBasicPage(uint8_t selected, bool encoderSelect, const char *noteName, const char *octaveName, const char *chordType, int8_t balArray[], float velArray[])
+{
+ if (isMessageActive())
+ {
+ renderMessage();
+ return;
+ }
+
+ display.fillRect(0, 0, 128, 32, BLACK);
+
+ dispParamLabel(0, 10, 32, 18, selected == 0, 1, encoderSelect, true, noteName, FONT_VALUES, 1, true);
+ dispParamLabel(32, 10, 32, 18, selected == 1, 1, encoderSelect, true, octaveName, FONT_VALUES, 1, true);
+ dispParamLabel(0, 0, 128, 10, selected == 3, 0, encoderSelect, true, chordType, FONT_LABELS, 1, true);
+
+ const uint8_t width = 10;
+ const uint8_t height = 16;
+ const uint8_t highHeight = 10;
+ const uint8_t space = 3;
+ const uint8_t totalWidth = width + space * 2;
+ const uint8_t startY = 11;
+ const uint8_t endY = startY + height; // 27
+
+ // const uint8_t startX = 64 - (((totalWidth) * 4) / 2); // 64 is width of duders
+
+ const uint8_t startX = 64; // 64 is width of duders
+
+ for (uint8_t i = 0; i < 4; i++)
+ {
+ uint8_t yPos = map(velArray[i], 0.0f, 1.0f, (float)endY, (float)startY);
+
+ int bal = balArray[i];
+ if (bal <= -10)
+ continue;
+
+ if (bal == 0)
+ {
+ display.fillRect(startX + (totalWidth * i) + space, yPos, width, height, WHITE);
+
+ // Eyes
+ // xx xx xx xx xx
+ // xx oo xx oo xx
+ // xx oo xx oo xx
+ // xx xx xx xx xx
+ display.fillRect(startX + (totalWidth * i) + space + 2, yPos + 2, 2, 4, BLACK);
+ display.fillRect(startX + (totalWidth * i) + space + 6, yPos + 2, 2, 4, BLACK);
+ }
+ else if (bal < 0)
+ {
+ yPos += 2;
+ display.fillRect(startX + (totalWidth * i) + space - 2, yPos - 2, width + 4, height + 4, WHITE);
+ display.fillRect(startX + (totalWidth * i) + space, yPos, width, height, BLACK);
+
+ display.fillRect(startX + (totalWidth * i) + space + 2, yPos + 2, 2, 2, WHITE);
+ display.fillRect(startX + (totalWidth * i) + space + 6, yPos + 2, 2, 2, WHITE);
+ }
+ else if (bal > 0)
+ {
+ display.fillRect(startX + (totalWidth * i) + space, yPos, width, highHeight, WHITE);
+
+ display.fillRect(startX + (totalWidth * i) + space + 2, yPos + 2, 2, 4, BLACK);
+ display.fillRect(startX + (totalWidth * i) + space + 6, yPos + 2, 2, 4, BLACK);
+ }
+ }
+
+ display.fillRect(startX, 28, 64, 10, BLACK);
+
+ if (selected == 2 && encoderSelect)
+ {
+ display.fillRect(startX + 32 - 1, 28, 2, 4, WHITE);
+ display.fillRect(startX + 32 - 3, 28 + 2, 6, 2, WHITE);
+ }
+ else if (selected == 2 && !encoderSelect)
+ {
+ display.fillRect(startX + 2, 28, 64 - 4, 2, WHITE);
+ }
+}
+
+void OmxDisp::dispChordBalance()
+{
+ const uint8_t width = 10;
+ const uint8_t height = 16;
+ const uint8_t highHeight = 10;
+ const uint8_t space = 3;
+ const uint8_t totalWidth = width + space * 2;
+ const uint8_t startY = 5;
+ const uint8_t endY = startY + height;
+
+ const uint8_t startX = 64 - (((totalWidth) * 4) / 2);
+
+ display.fillRect(0, 0, 128, 32, BLACK);
+
+ for (uint8_t i = 0; i < 4; i++)
+ {
+ uint8_t yPos = map(chordVelArray_[i], 0.0f, 1.0f, (float)endY, (float)startY);
+
+ // Serial.println("ypos: " + String(yPos));
+
+ int bal = chordBalArray_[i];
+
+ // Serial.println("bal: " + String(bal));
+
+ if (bal <= -10)
+ continue;
+
+ if (bal == 0)
+ {
+ display.fillRect(startX + (totalWidth * i) + space, yPos, width, height, WHITE);
+
+ // Eyes
+ // xx xx xx xx xx
+ // xx oo xx oo xx
+ // xx oo xx oo xx
+ // xx xx xx xx xx
+ display.fillRect(startX + (totalWidth * i) + space + 2, yPos + 2, 2, 4, BLACK);
+ display.fillRect(startX + (totalWidth * i) + space + 6, yPos + 2, 2, 4, BLACK);
+ }
+ else if (bal < 0)
+ {
+ yPos += 2;
+ display.fillRect(startX + (totalWidth * i) + space - 2, yPos - 2, width + 4, height + 4, WHITE);
+ display.fillRect(startX + (totalWidth * i) + space, yPos, width, height, BLACK);
+
+ display.fillRect(startX + (totalWidth * i) + space + 2, yPos + 2, 2, 2, WHITE);
+ display.fillRect(startX + (totalWidth * i) + space + 6, yPos + 2, 2, 2, WHITE);
+ }
+ else if (bal > 0)
+ {
+ display.fillRect(startX + (totalWidth * i) + space, yPos, width, highHeight, WHITE);
+
+ display.fillRect(startX + (totalWidth * i) + space + 2, yPos + 2, 2, 4, BLACK);
+ display.fillRect(startX + (totalWidth * i) + space + 6, yPos + 2, 2, 4, BLACK);
+ }
+ }
+
+ // Serial.println("");
+
+ display.fillRect(0, endY, 128, 32, BLACK);
+}
+
+void OmxDisp::dispLabelParams(int8_t selected, bool encSelActive, const char *labels[], uint8_t labelCount, bool centered)
+{
+ u8g2_display.setFontMode(1);
+ u8g2_display.setFont(FONT_LABELS);
+ u8g2_display.setCursor(0, 0);
+
+ uint8_t labelWidth = 128 / labelCount; // 8
+
+ for (uint8_t i = 0; i < labelCount; i++)
+ {
+ bool invert = false;
+ // Label Selected
+ if (i == selected)
+ {
+ if (encSelActive == false)
+ {
+ display.fillRect(i * labelWidth, 0, labelWidth, 10, WHITE);
+ invert = true;
+ }
+ else
+ {
+ display.fillRect(i * labelWidth, 0, labelWidth, 10, WHITE);
+ display.fillRect(i * labelWidth + 1, 0 + 1, labelWidth - 2, 10 - 2, BLACK);
+ }
+ }
+
+ invertColor(invert);
+ if (centered)
+ {
+ u8g2centerText(labels[i], i * labelWidth + 2, hline - 2, labelWidth - 4, 10);
+ }
+ else
+ {
+ u8g2leftText(labels[i], i * labelWidth + 2, hline - 2, labelWidth - 4, 10);
+ }
+ }
+}
+
+void OmxDisp::dispParamLabel(uint8_t x, uint8_t y, uint8_t width, uint8_t height, bool selected, uint8_t selectionType, bool encSelActive, bool showLabel, const char *label, const uint8_t *font, int8_t labelYOffset, bool centered)
+{
+ bool invert = false;
+ // Label Selected
+ if (selected && encSelActive)
+ {
+ if (selectionType == 0)
+ {
+ display.drawRect(x, y, width, height, WHITE);
+ // display.fillRect(x + 1, 0 + 1, width - 2, 10 - 2, BLACK);
+ }
+ else if (selectionType == 1)
+ {
+ display.fillRect(x + width / 2 - 1, y + height, 2, 4, WHITE);
+ display.fillRect(x + width / 2 - 3, y + height + 2, 6, 2, WHITE);
+ }
+ }
+ else if (selected && !encSelActive)
+ {
+ if (selectionType == 0)
+ {
+ display.fillRect(x, y, width, height, WHITE);
+ invert = true;
+ }
+ else if (selectionType == 1)
+ {
+ display.fillRect(x + 2, y + height, width - 4, 2, WHITE);
+ }
+ }
+
+ if (showLabel)
+ {
+ u8g2_display.setFontMode(1);
+ u8g2_display.setFont(font);
+ u8g2_display.setCursor(0, 0);
+
+ invertColor(invert);
+ if (centered)
+ {
+ u8g2centerText(label, x, y + height / 2 + labelYOffset, width, height);
+ }
+ else
+ {
+ u8g2leftText(label, x + 2, y + height / 2 + labelYOffset, width - 4, height);
+ }
+ }
+}
+
+void OmxDisp::dispPageIndicators2(uint8_t numPages, int8_t selected)
+{
+ int16_t indicatorWidth = 6;
+ int16_t indicatorYPos = 32;
+ int16_t segment = (6 + 6);
+
+ int16_t start = (128 - (segment * numPages)) / 2.0;
+
+ // Serial.println((String)"start: " + start + " indicatorYPos: " + indicatorYPos + " segment: " + segment);
+
+ for (uint8_t i = 0; i < numPages; i++)
+ {
+ int16_t h = ((i == selected) ? 2 : 1);
+
+ display.fillRect(start + (i * segment), indicatorYPos - h, indicatorWidth, h, WHITE);
+ }
+}
+
+void OmxDisp::dispPageIndicators(int page, bool selected)
+{
+ if (selected)
+ {
+ display.fillRect(43 + (page * 12), 30, 6, 2, WHITE);
+ }
+ else
+ {
+ display.fillRect(43 + (page * 12), 31, 6, 1, WHITE);
+ }
+}
+
+void OmxDisp::dispMode()
+{
+ animPos = constrain(animPos, 0, 3);
+
+ animTimer -= sysSettings.timeElasped;
+
+ if(animTimer <= 0)
+ {
+ animPos = (animPos + 1) % 4;
+ animTimer = 100000;
+ setDirty();
+ }
+
+ if (isDirty())
+ {
+ u8g2_display.setFontMode(0);
+ u8g2_display.setFont(FONT_SYMB_BIG);
+ u8g2centerText(loaderAnim[animPos], 80, 10, 32, 32); // "\u00BB\u00AB" // // dice: "\u2685"
+
+ // labels formatting
+ u8g2_display.setFontMode(1);
+ u8g2_display.setFont(FONT_BIG);
+ u8g2_display.setCursor(0, 0);
+
+ u8g2_display.setForegroundColor(WHITE);
+ u8g2_display.setBackgroundColor(BLACK);
+
+ const char *displaymode = "";
+ if (sysSettings.newmode != sysSettings.omxMode && encoderConfig.enc_edit)
+ {
+ displaymode = modes[sysSettings.newmode]; // display.print(modes[sysSettings.newmode]);
+ }
+ else if (encoderConfig.enc_edit)
+ {
+ displaymode = modes[sysSettings.omxMode]; // display.print(modes[mode]);
+ }
+ u8g2centerText(displaymode, 2, 20, 75, 32);
+ }
+}
+
+void OmxDisp::setDirty()
+{
+ dirtyDisplay = true;
+}
+
+void OmxDisp::UpdateMessageTextTimer()
+{
+ if (messageTextTimer > 0)
+ {
+ messageTextTimer -= sysSettings.timeElasped;
+ if (messageTextTimer <= 0)
+ {
+ setDirty();
+ messageTextTimer = 0;
+ }
+ }
+}
+
+void OmxDisp::showDisplay()
+{
+ if (dirtyDisplay)
+ {
+ if (dirtyDisplayTimer > displayRefreshRate)
+ {
+ display.display();
+ dirtyDisplay = false;
+ dirtyDisplayTimer = 0;
+ }
+ }
+}
+
+void OmxDisp::bumpDisplayTimer()
+{
+ dirtyDisplayTimer = displayRefreshRate + 1;
+}
+
+void OmxDisp::drawEuclidPattern(bool singleView, bool *pattern, uint8_t steps, uint8_t yPos, bool selected, bool isPlaying, uint8_t seqPos)
+{
+ if (isMessageActive())
+ {
+ renderMessage();
+ return;
+ }
+
+ const bool selectAsLine = false;
+
+ int16_t startSpacing = singleView ? 0 : 6;
+ int16_t patWidth = gridw - startSpacing;
+
+ if (selected)
+ {
+ if (selectAsLine)
+ {
+ display.drawLine(0, yPos, gridw, yPos, WHITE);
+ patWidth = gridw;
+ startSpacing = 0;
+ }
+ else
+ {
+ display.fillRect(0, yPos - 3, 3, 3, WHITE);
+ display.drawPixel(1, yPos - 2, BLACK);
+ }
+ }
+
+ if (steps == 0)
+ {
+ return;
+ }
+
+ int16_t steponHeight = singleView ? 8 : 5;
+ // int16_t steponWidth = 2;
+ int16_t stepoffHeight = 2;
+ // int16_t stepoffWidth = 2;
+ // int16_t halfh = gridh / 2;
+ // int16_t halfw = gridw / 2;
+
+ float stepint = (float)patWidth / (float)steps;
+
+ for (int i = 0; i < steps; i++)
+ {
+ int16_t xPos = startSpacing + (stepint * i);
+ // int16_t yPos = halfh;
+
+ uint8_t w = 2;
+ if (isPlaying && i == seqPos)
+ {
+ w = 4;
+ xPos -= 1;
+ }
+
+ if (pattern[i])
+ {
+ display.fillRect(xPos, yPos - steponHeight, w, steponHeight, WHITE);
+ }
+ else
+ {
+ display.fillRect(xPos, yPos - stepoffHeight, w, stepoffHeight, WHITE);
+ }
+
+ // if(i == seqPos)
+ // {
+ // display.fillRect(xPos, yPos, stepoffWidth, stepoffWidth, WHITE);
+
+ // // display.drawPixel(xPos, yPos, WHITE);
+ // }
+ }
+
+ // if (isPlaying)
+ // {
+ // uint8_t seqPos
+ // int16_t xPos = (gridw - startSpacing) * playheadPerc + startSpacing;
+
+ // display.drawPixel(xPos, yPos, WHITE);
+ // }
+
+ // omxDisp.setDirty();
+}
+
+OmxDisp omxDisp;
diff --git a/Archive/OMX-27-firmware/src/hardware/omx_disp.h b/Archive/OMX-27-firmware/src/hardware/omx_disp.h
new file mode 100644
index 00000000..1970d9ec
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/hardware/omx_disp.h
@@ -0,0 +1,128 @@
+#pragma once
+#include "../config.h"
+
+// MESSAGE DISPLAY
+const int MESSAGE_TIMEOUT_US = 500000;
+
+class OmxDisp
+{
+public:
+ // Should make into function
+
+ const char *legends[4] = {"", "", "", ""};
+ int legendVals[4] = {0, 0, 0, 0};
+ int dispPage = 0;
+ const char *legendText[4] = {"", "", "", ""};
+ bool useLegendString[4] = {false, false, false, false};
+ String legendString[4] = {"12345", "12345", "12345", "12345"};
+
+ OmxDisp();
+ void setup();
+ void clearDisplay();
+ void drawStartupScreen();
+ void displayMessage(String msg);
+ void displayMessage(const char *msg);
+ void displayMessagef(const char *fmt, ...);
+ void displayMessageTimed(String msg, uint8_t secs);
+ void displaySpecialMessage(uint8_t msgType, String msg, uint8_t secs);
+
+ bool isMessageActive();
+
+ void dispGridBoxes();
+ void invertColor(bool flip);
+ void dispValBox(int v, int16_t n, bool inv);
+ void dispSymbBox(const char *v, int16_t n, bool inv);
+ void dispGenericMode(int selected);
+
+ void dispGenericMode2(uint8_t numPages, int8_t selectedPage, int8_t selectedParam, bool encSelActive);
+
+ // Displays a label and page numbers
+ void dispGenericModeLabel(const char *label, uint8_t numPages, int8_t selectedPage);
+ void dispGenericModeLabelDoubleLine(const char *label1, const char *label2, uint8_t numPages, int8_t selectedPage);
+ void dispGenericModeLabelSmallText(const char *label, uint8_t numPages, int8_t selectedPage);
+
+ // Displays a header and options below
+ // Good for something like a yes/no box
+ void dispOptionCombo(const char * header, const char *options[], uint8_t optionCount, uint8_t selected, bool encSelActive);
+
+ void dispChar16(const char *charArray[], uint8_t charCount, uint8_t selected, uint8_t numPages, int8_t selectedPage, bool encSelActive, bool showLabels, const char *labels[], uint8_t labelCount);
+
+ // Renders values as bars
+ void dispValues16(int8_t valueArray[], uint8_t valueCount, int8_t minValue, int8_t maxValue, bool centered, uint8_t selected, uint8_t numPages, int8_t selectedPage, bool encSelActive, bool showLabels, const char *labels[], uint8_t labelCount);
+
+ void dispParamBar(int8_t potValue, int8_t targetValue, int8_t minValue, int8_t maxValue, bool pickedUp, bool centered, const char* bankName, const char* paramName);
+
+ // Displays slots for midifx or something else in future
+ void dispSlots(const char *slotNames[], uint8_t slotCount, uint8_t selected, uint8_t animPos, bool encSelActive, bool showLabels, const char *labels[], uint8_t labelCount);
+
+ // Displays multiple slots up to slotCount all centered
+ void dispCenteredSlots(const char *slotNames[], uint8_t slotCount, uint8_t selected, bool encoderSelect, bool showLabels, bool centerLabels, const char *labels[], uint8_t labelCount);
+
+ // noteNumbers should be array of 6
+ void dispKeyboard(int rootNote, int noteNumbers[], bool showLabels, const char *labels[], uint8_t labelCount);
+
+ void dispChordBasicPage(uint8_t selected, bool encoderSelect, const char *noteName, const char *octaveName, const char *chordType, int8_t balArray[], float velArray[]);
+ void chordBalanceMsg(int8_t balArray[], float velArray[], uint8_t secs);
+
+ void dispLabelParams(int8_t selected, bool encSelActive, const char *labels[], uint8_t labelCount, bool centered);
+
+ void dispPageIndicators(int page, bool selected);
+ void dispPageIndicators2(uint8_t numPages, int8_t selected);
+ void dispMode();
+
+ void testdrawrect();
+ void drawLoading();
+
+ void setDirty();
+ bool isDirty() { return dirtyDisplay; }
+
+ void showDisplay();
+
+ void bumpDisplayTimer();
+
+ void clearLegends();
+ void setLegend(uint8_t index, const char* label, int value);
+ void setLegend(uint8_t index, const char* label, bool isOff, int value);
+ void setLegend(uint8_t index, const char* label, const char* text);
+ void setLegend(uint8_t index, const char* label, bool isOff, const char* text);
+ void setLegend(uint8_t index, const char* label, String text);
+ void setLegend(uint8_t index, const char* label, bool isOff, String text);
+ void setLegend(uint8_t index, const char* label, bool value);
+
+
+ void setSubmode(int submode);
+
+ void UpdateMessageTextTimer();
+
+ void drawEuclidPattern(bool singleView, bool *pattern, uint8_t steps, uint8_t yPos, bool selected, bool isPlaying, uint8_t seqPos);
+
+private:
+ int hline = 8;
+ int messageTextTimer = 0;
+ bool dirtyDisplay = false;
+
+ uint8_t animPos = 0;
+ int animTimer = 0;
+
+ String currentMsg;
+ uint8_t specialMsgType_ = 0;
+
+ int8_t chordBalArray_[4];
+ float chordVelArray_[4];
+
+ elapsedMillis dirtyDisplayTimer = 0;
+ unsigned long displayRefreshRate = 60;
+
+ void dispParamLabel(uint8_t x, uint8_t y, uint8_t width, uint8_t height, bool selected, uint8_t selectionType, bool encSelActive, bool showLabel, const char *label, const uint8_t *font, int8_t labelYOffset, bool centered);
+
+ void u8g2centerText(const char *s, int16_t x, int16_t y, uint16_t w, uint16_t h);
+ void u8g2leftText(const char *s, int16_t x, int16_t y, uint16_t w, uint16_t h);
+ void u8g2centerNumber(int n, uint16_t x, uint16_t y, uint16_t w, uint16_t h);
+ void renderMessage();
+
+ void dispChordBalance();
+
+ bool validateLegendIndex(uint8_t index);
+};
+
+extern OmxDisp omxDisp;
diff --git a/Archive/OMX-27-firmware/src/hardware/omx_keypad.cpp b/Archive/OMX-27-firmware/src/hardware/omx_keypad.cpp
new file mode 100644
index 00000000..868c3711
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/hardware/omx_keypad.cpp
@@ -0,0 +1,131 @@
+#include "omx_keypad.h"
+
+/**************************************************************************/
+/*!
+ @brief default constructor
+ @param holdThreshold time a button must be held to trigger the "held" state
+ @param clickWindow time for registering multiple clicks, resets on each click
+ @param userKeymap a multidimensional array of key characters
+ @param row an array of GPIO pins that are connected to each row of the
+keypad
+ @param col an array of GPIO pins that are connected to each column of the
+keypad
+ @param numRows the number of rows on the keypad
+ @param numCols the number of columns on the keypad
+*/
+/**************************************************************************/
+OMXKeypad::OMXKeypad(uint32_t holdThreshold, uint32_t clickWindow, byte *userKeymap, byte *row, byte *col, int numRows, int numCols) : numRows(numRows),
+ numCols(numCols),
+ holdThreshold(holdThreshold),
+ clickWindow(clickWindow),
+ keypad(userKeymap, row, col, numRows, numCols),
+ keys(numRows * numCols)
+{
+}
+
+void OMXKeypad::tick()
+{
+ keypad.tick();
+
+ uint32_t now = millis();
+ while (keypad.available())
+ {
+ keypadEvent e = keypad.read();
+ // the key isn't an index.
+ uint8_t index = (e.bit.ROW * numCols) + e.bit.COL;
+ keystate *key = &(keys[index]);
+
+ switch (e.bit.EVENT)
+ {
+ case KEY_JUST_PRESSED:
+ // first press.
+ if (key->lastClickedAt == 0)
+ {
+ key->key = e.bit.KEY;
+ key->index = index;
+ key->held = false;
+ if (key->releasedAt < now - clickWindow)
+ {
+ key->clicks = 0;
+ }
+ active.push_back(key);
+ }
+
+ key->lastClickedAt = now;
+ key->down = true;
+ key->held = false;
+ key->quickClicked = false;
+ // "press" is always available
+ _available.push_back(key); // this is what triggers the key to show up with a state change
+ break;
+ case KEY_JUST_RELEASED:
+ key->down = false;
+ key->clicks++;
+ key->releasedAt = now;
+
+ if (key->held)
+ {
+ // hold release event.
+ key->held = false;
+ }
+ key->quickClicked = (now - key->lastClickedAt) <= clickWindow;
+ _available.push_back(key); // on key release, this is the only event added.
+ break;
+ default:
+ // unknown event
+ break;
+ };
+ }
+
+ // exit early if there are no active keys to update.
+ if (active.size() == 0)
+ return;
+
+ // Check if any active keys are ready to become available.
+ uint32_t click_window_close = now - clickWindow;
+ uint32_t held = now - holdThreshold;
+ auto it = active.begin();
+ while (it != active.end())
+ {
+ auto key = *it;
+ if (key->down && key->lastClickedAt < held)
+ {
+ key->held = true;
+ _available.push_back(key);
+ active.erase(it);
+ }
+ else if (!key->down && key->lastClickedAt < click_window_close)
+ {
+ // _available.push_back(key);
+ active.erase(it);
+ // } else if (!key->down && key->lastClickedAt < now) {
+ // active.erase(it);
+ }
+ else
+ {
+ // it is not ready to become active, move to next.
+ it++;
+ }
+ }
+}
+
+OMXKeypadEvent OMXKeypad::next()
+{
+ if (!available())
+ {
+ return OMXKeypadEvent{0, 0, false, false, false};
+ }
+
+ auto key = _available.back();
+ _available.pop_back();
+
+ // Simple press event.
+ if (key->down && !key->held)
+ {
+ return OMXKeypadEvent{key->key, key->clicks, false, true, false};
+ }
+
+ // Click or hold event
+ key->lastClickedAt = 0;
+ return OMXKeypadEvent{key->key, key->clicks, key->held, key->down, key->quickClicked};
+}
diff --git a/Archive/OMX-27-firmware/src/hardware/omx_keypad.h b/Archive/OMX-27-firmware/src/hardware/omx_keypad.h
new file mode 100644
index 00000000..4da7f573
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/hardware/omx_keypad.h
@@ -0,0 +1,76 @@
+#pragma once
+
+#include
+#include
+#include
+
+#define MAX_CONCURRENT_KEYS 10
+
+// Forward declare
+struct OMXKeypadEvent;
+
+/**
+ * Keep track of button states.
+ */
+class OMXKeypad
+{
+private:
+ // This could be optimized to use less space.
+ struct keystate
+ {
+ // constructor for vector initializer
+ keystate() : lastClickedAt(0){};
+
+ uint8_t index;
+ uint8_t key;
+ bool held;
+ bool down;
+ bool quickClicked;
+ uint8_t clicks = 0;
+ uint32_t lastClickedAt;
+ uint32_t releasedAt;
+ };
+
+ int numRows;
+ int numCols;
+ uint32_t holdThreshold;
+ uint32_t clickWindow;
+ Adafruit_Keypad keypad;
+ std::vector keys;
+ std::vector active;
+ std::vector _available;
+
+public:
+ OMXKeypad(uint32_t holdThreshold, uint32_t clickWindow, byte *userKeymap,
+ byte *row, byte *col, int numRows, int numCols);
+ inline void begin() { keypad.begin(); }
+ void tick();
+
+ inline bool available() { return _available.size() > 0; }
+ OMXKeypadEvent next();
+};
+
+struct OMXKeypadEvent
+{
+ OMXKeypadEvent(uint8_t key, uint8_t clicks, bool held, bool down, bool quickClicked) : _key(key),
+ _clicks(clicks),
+ _held(held),
+ _down(down),
+ _quickClicked(quickClicked)
+ {
+ }
+
+private:
+ uint8_t _key;
+ uint8_t _clicks;
+ bool _held;
+ bool _down;
+ bool _quickClicked;
+
+public:
+ inline uint8_t key() { return _key; }
+ inline bool down() { return _down; }
+ inline bool held() { return _held; }
+ inline bool quickClicked() { return _quickClicked; }
+ inline uint8_t clicks() { return _clicks; }
+};
diff --git a/Archive/OMX-27-firmware/src/hardware/omx_leds.cpp b/Archive/OMX-27-firmware/src/hardware/omx_leds.cpp
new file mode 100644
index 00000000..35b9ff6c
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/hardware/omx_leds.cpp
@@ -0,0 +1,405 @@
+#include "omx_leds.h"
+#include "../consts/consts.h"
+#include "../consts/colors.h"
+
+Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800);
+
+// OmxLeds::OmxLeds(){
+// Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800);
+// }
+
+void OmxLeds::initSetup()
+{
+ strip.begin(); // INITIALIZE NeoPixel strip object (REQUIRED)
+ strip.show(); // Turn OFF all pixels ASAP
+ strip.setBrightness(LED_BRIGHTNESS); // Set BRIGHTNESS to about 1/5 (max = 255)
+ for (int i = 0; i < LED_COUNT; i++)
+ { // For each pixel...
+ strip.setPixelColor(i, HALFWHITE);
+ strip.show(); // Send the updated pixel colors to the hardware.
+ delay(5); // Pause before next pass through loop
+ }
+ rainbow(5); // rainbow startup pattern
+ delay(500);
+
+ // clear LEDs
+ strip.fill(0, 0, LED_COUNT);
+ strip.show();
+
+ delay(100);
+}
+
+void OmxLeds::updateBlinkStates()
+{
+ blinkInterval = clockConfig.step_delay * 2;
+ unsigned long slowBlinkInterval = blinkInterval * 2;
+
+ if (blink_msec >= blinkInterval)
+ {
+ blinkState = !blinkState;
+ blink_msec = 0;
+
+ for (uint8_t i = 0; i < 10; i++)
+ {
+ uint8_t patMax = ((i + 1) * 2) + blinkPatternDelay_;
+ blinkPatPos[i] = (blinkPatPos[i] + 1) % patMax;
+ }
+
+ setDirty();
+ }
+ if (slow_blink_msec >= slowBlinkInterval)
+ {
+ slowBlinkState = !slowBlinkState;
+ slow_blink_msec = 0;
+ setDirty();
+ }
+}
+
+int OmxLeds::getKeyColor(MusicScales *scale, int pixel)
+{
+ if (scale == nullptr)
+ return LEDOFF;
+
+ if (scaleConfig.scalePattern == -1)
+ {
+ return LEDOFF;
+ }
+ else
+ {
+ // if(sysSettings.omxMode == MODE_MIDI && AUX_HELD) {
+ // if(pixel == 1 || pixel == 2 || pixel == 3 || pixel == 4 || pixel == 11 || pixel == 12) {
+ // return LEDOFF;
+ // }
+ // }
+
+ if (scaleConfig.group16)
+ {
+ return scale->getGroup16Color(pixel);
+ }
+ else
+ {
+ int noteInOct = notes[pixel] % 12;
+ return scale->getScaleColor(noteInOct);
+ }
+ }
+}
+
+void OmxLeds::drawMidiLeds(MusicScales *scale)
+{
+ // updateBlinkStates();
+ // blinkInterval = clockConfig.step_delay*2;
+
+ // if (blink_msec >= blinkInterval){
+ // blinkState = !blinkState;
+ // blink_msec = 0;
+ // }
+
+ if (midiSettings.midiAUX)
+ {
+ // Blink left/right keys for octave select indicators.
+ auto color1 = blinkState ? LIME : LEDOFF;
+ auto color2 = blinkState ? MAGENTA : LEDOFF;
+ auto color3 = blinkState ? ORANGE : LEDOFF;
+ auto color4 = blinkState ? RBLUE : LEDOFF;
+
+ for (int q = 1; q < LED_COUNT; q++)
+ {
+ if (midiSettings.midiKeyState[q] == -1)
+ {
+ if (colorConfig.midiBg_Hue == 0)
+ {
+ strip.setPixelColor(q, LEDOFF);
+ }
+ else if (colorConfig.midiBg_Hue == 32)
+ {
+ strip.setPixelColor(q, LOWWHITE);
+ }
+ else
+ {
+ strip.setPixelColor(q, strip.ColorHSV(colorConfig.midiBg_Hue, colorConfig.midiBg_Sat, colorConfig.midiBg_Brightness));
+ }
+ }
+ }
+ strip.setPixelColor(0, RED);
+ strip.setPixelColor(1, color1);
+ strip.setPixelColor(2, color2);
+ strip.setPixelColor(11, color3);
+ strip.setPixelColor(12, color4);
+
+ strip.setPixelColor(10, color3); // MidiFX key
+
+ // Macros
+ }
+ else
+ {
+ // AUX key
+ strip.setPixelColor(0, LEDOFF);
+
+ // Other keys
+ if (!sysSettings.screenSaverMode)
+ {
+ // clear not held leds
+ for (int q = 1; q < LED_COUNT; q++)
+ {
+ if (midiSettings.midiKeyState[q] == -1)
+ {
+ if (colorConfig.midiBg_Hue == 0)
+ {
+ strip.setPixelColor(q, getKeyColor(scale, q)); // set off or in scale
+ }
+ else if (colorConfig.midiBg_Hue == 32)
+ {
+ strip.setPixelColor(q, LOWWHITE);
+ }
+ else
+ {
+ strip.setPixelColor(q, strip.ColorHSV(colorConfig.midiBg_Hue, colorConfig.midiBg_Sat, colorConfig.midiBg_Brightness));
+ }
+ }
+ }
+ }
+ }
+ dirtyPixels = true;
+}
+
+bool OmxLeds::getBlinkState()
+{
+ return blinkState;
+}
+bool OmxLeds::getSlowBlinkState()
+{
+ return slowBlinkState;
+}
+
+bool OmxLeds::getBlinkPattern(uint8_t numberOfBlinks)
+{
+ if (numberOfBlinks < 1 || numberOfBlinks > 10)
+ return false;
+
+ // Serial.println("blinkPatPos: " + String(blinkPatPos[numberOfBlinks - 1]));
+
+ if (blinkPatPos[numberOfBlinks - 1] >= (numberOfBlinks * 2))
+ {
+ // 4 = x0x0x0x00000
+ // Serial.println("blinkPatPos delayed");
+ return false; // the delay
+ }
+
+ bool blink = (blinkPatPos[numberOfBlinks - 1] % 2 == 0); // the blink
+
+ // if(blink)
+ // {
+ // Serial.println("Blink On");
+ // }
+ // else
+ // {
+ // Serial.println("Blink Off");
+ // }
+
+ return blink;
+}
+
+void OmxLeds::setAllLEDS(int R, int G, int B)
+{
+ for (int i = 0; i < LED_COUNT; i++)
+ { // For each pixel...
+ strip.setPixelColor(i, strip.Color(R, G, B));
+ }
+ setDirty();
+}
+
+void OmxLeds::drawOctaveKeys(uint8_t octaveDownKey, uint8_t octaveUpKey, int8_t octaveVal)
+{
+ if (octaveVal == 0)
+ {
+ strip.setPixelColor(octaveDownKey, octDnColor);
+ strip.setPixelColor(octaveUpKey, octUpColor);
+ }
+ else if (octaveVal > 0)
+ {
+ bool blinkOctave = getBlinkPattern(octaveVal);
+
+ strip.setPixelColor(octaveDownKey, octDnColor);
+ strip.setPixelColor(octaveUpKey, blinkOctave ? octUpColor : LEDOFF);
+ }
+ else
+ {
+ bool blinkOctave = getBlinkPattern(-octaveVal);
+
+ strip.setPixelColor(octaveDownKey, blinkOctave ? octDnColor : LEDOFF);
+ strip.setPixelColor(octaveUpKey, octUpColor);
+ }
+
+ setDirty();
+}
+
+void OmxLeds::setDirty()
+{
+ dirtyPixels = true;
+}
+
+bool OmxLeds::isDirty()
+{
+ return dirtyPixels;
+}
+
+void OmxLeds::showLeds()
+{
+ // are pixels dirty
+ if (dirtyPixels)
+ {
+ strip.show();
+ dirtyPixels = false;
+ }
+}
+
+void OmxLeds::rainbow(int wait)
+{
+ // Hue of first pixel runs 5 complete loops through the color wheel.
+ // Color wheel has a range of 65536 but it's OK if we roll over, so
+ // just count from 0 to 5*65536. Adding 256 to firstPixelHue each time
+ // means we'll make 5*65536/256 = 1280 passes through this outer loop:
+ for (long firstPixelHue = 0; firstPixelHue < 1 * 65536; firstPixelHue += 256)
+ {
+ for (int i = 0; i < strip.numPixels(); i++)
+ { // For each pixel in strip...
+ // Offset pixel hue by an amount to make one full revolution of the
+ // color wheel (range of 65536) along the length of the strip
+ // (strip.numPixels() steps):
+ int pixelHue = firstPixelHue + (i * 65536L / strip.numPixels());
+
+ // strip.ColorHSV() can take 1 or 3 arguments: a hue (0 to 65535) or
+ // optionally add saturation and value (brightness) (each 0 to 255).
+ // Here we're using just the single-argument hue variant. The result
+ // is passed through strip.gamma32() to provide 'truer' colors
+ // before assigning to each pixel:
+ strip.setPixelColor(i, strip.gamma32(strip.ColorHSV(pixelHue)));
+ }
+ strip.show(); // Update strip with new contents
+ delay(wait); // Pause for a moment
+ }
+}
+
+// Input a value 0 to 255 to get a color value.
+// The colours are a transition r - g - b - back to r.
+uint32_t OmxLeds::Wheel(byte WheelPos)
+{
+ WheelPos = 255 - WheelPos;
+ if (WheelPos < 85)
+ {
+ return strip.Color(255 - WheelPos * 3, 0, WheelPos * 3);
+ }
+ if (WheelPos < 170)
+ {
+ WheelPos -= 85;
+ return strip.Color(0, WheelPos * 3, 255 - WheelPos * 3);
+ }
+ WheelPos -= 170;
+ return strip.Color(WheelPos * 3, 255 - WheelPos * 3, 0);
+}
+
+void OmxLeds::colorWipe(byte red, byte green, byte blue, int SpeedDelay)
+{
+ for (uint16_t i = 0; i < strip.numPixels(); i++)
+ {
+ strip.setPixelColor(i, strip.Color(red, green, blue));
+ strip.show();
+ delay(SpeedDelay);
+ }
+}
+
+// Theatre-style crawling lights with rainbow effect
+void OmxLeds::theaterChaseRainbow(uint8_t wait)
+{
+ for (int j = 0; j < 256; j++)
+ { // cycle all 256 colors in the wheel
+ for (int q = 0; q < 3; q++)
+ {
+ for (uint16_t i = 0; i < strip.numPixels(); i = i + 3)
+ {
+ strip.setPixelColor(i + q, Wheel((i + j) % 255)); // turn every third pixel on
+ }
+ strip.show();
+ delay(wait);
+ for (uint16_t i = 0; i < strip.numPixels(); i = i + 3)
+ {
+ strip.setPixelColor(i + q, 0); // turn every third pixel off
+ }
+ }
+ }
+}
+
+void OmxLeds::CylonBounce(byte red, byte green, byte blue, int EyeSize, int SpeedDelay, int ReturnDelay, int start, int end)
+{
+ for (int i = start; i < end - EyeSize - 2; i++)
+ {
+ setAllLEDS(0, 0, 0);
+ strip.setPixelColor(i, strip.Color(red / 10, green / 10, blue / 10));
+ for (int j = 1; j <= EyeSize; j++)
+ {
+ strip.setPixelColor(i + j, strip.Color(red, green, blue));
+ }
+ strip.setPixelColor(i + EyeSize + 1, strip.Color(red / 10, green / 10, blue / 10));
+ strip.show();
+ delay(SpeedDelay);
+ }
+ delay(ReturnDelay);
+ for (int i = end - EyeSize - 2; i > start; i--)
+ {
+ setAllLEDS(0, 0, 0);
+ strip.setPixelColor(i, strip.Color(red / 10, green / 10, blue / 10));
+ for (int j = 1; j <= EyeSize; j++)
+ {
+ strip.setPixelColor(i + j, strip.Color(red, green, blue));
+ }
+ strip.setPixelColor(i + EyeSize + 1, strip.Color(red / 10, green / 10, blue / 10));
+ strip.show();
+ delay(SpeedDelay);
+ }
+ delay(ReturnDelay);
+}
+
+void OmxLeds::RolandFill(byte red, byte green, byte blue, int start, int end, int SpeedDelay)
+{
+ for (uint16_t j = end; j > start; j--)
+ {
+ for (uint16_t i = start; i < end; i++)
+ {
+ strip.setPixelColor(i, strip.Color(red, green, blue));
+ strip.show();
+ if (i < j)
+ {
+ strip.setPixelColor(i, 0);
+ }
+ if (!sysSettings.screenSaverMode)
+ {
+ return;
+ }
+ }
+ strip.setPixelColor(j, strip.Color(red, green, blue));
+ strip.show();
+ }
+ for (uint16_t j = end; j > start - 1; j--)
+ {
+ for (uint16_t i = start; i < end + 1; i++)
+ {
+ strip.setPixelColor(i, strip.Color(red, green, blue));
+ strip.show();
+ if (i > j)
+ {
+ strip.setPixelColor(i, 0);
+ }
+ if (!sysSettings.screenSaverMode)
+ {
+ return;
+ }
+ }
+ if (j != start)
+ {
+ strip.setPixelColor(j, strip.Color(red, green, blue));
+ }
+ strip.show();
+ }
+}
+
+OmxLeds omxLeds;
diff --git a/Archive/OMX-27-firmware/src/hardware/omx_leds.h b/Archive/OMX-27-firmware/src/hardware/omx_leds.h
new file mode 100644
index 00000000..f1af5ae4
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/hardware/omx_leds.h
@@ -0,0 +1,75 @@
+#pragma once
+
+#include "../config.h"
+#include "../utils/music_scales.h"
+
+#include
+
+// Declare NeoPixel strip object
+extern Adafruit_NeoPixel strip;
+
+class OmxLeds
+{
+public:
+ static const int octDnColor = ORANGE;
+ static const int octUpColor = RBLUE;
+
+ // OmxLeds() : strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800){};
+
+ OmxLeds(){};
+
+ void initSetup();
+
+ void updateBlinkStates();
+
+ int getKeyColor(MusicScales *scale, int pixel);
+ void drawMidiLeds(MusicScales *scale);
+
+ // clears dirty, transmits pixel data if dirty.
+ void showLeds();
+
+ bool getBlinkState();
+ bool getSlowBlinkState();
+
+ // Blinks for numberOfBlinks then a delay
+ bool getBlinkPattern(uint8_t numberOfBlinks);
+
+ // void setPixelColor(uint16_t n, uint8_t r, uint8_t g, uint8_t b);
+
+ void setAllLEDS(int R, int G, int B);
+
+ void drawOctaveKeys(uint8_t octaveDownKey, uint8_t octaveUpKey, int8_t octaveVal);
+
+ void setDirty();
+ bool isDirty();
+
+ // Rainbow cycle along whole strip. Pass delay time (in ms) between frames.
+ void rainbow(int wait);
+
+ // #### COLOR FUNCTIONS
+ // Input a value 0 to 255 to get a color value.
+ // The colours are a transition r - g - b - back to r.
+ uint32_t Wheel(byte WheelPos);
+ void colorWipe(byte red, byte green, byte blue, int SpeedDelay);
+ // Theatre-style crawling lights with rainbow effect
+ void theaterChaseRainbow(uint8_t wait);
+ void CylonBounce(byte red, byte green, byte blue, int EyeSize, int SpeedDelay, int ReturnDelay, int start, int end);
+ void RolandFill(byte red, byte green, byte blue, int start, int end, int SpeedDelay);
+
+private:
+ unsigned long blinkInterval = clockConfig.clockbpm * 2;
+ bool blinkState = false;
+ bool slowBlinkState = false;
+
+ bool dirtyPixels = false;
+
+ elapsedMillis blink_msec = 0;
+ elapsedMillis slow_blink_msec = 0;
+
+ uint8_t blinkPatPos[10];
+ const uint8_t blinkPatternDelay_ = 2;
+
+
+};
+
+extern OmxLeds omxLeds;
diff --git a/Archive/OMX-27-firmware/src/hardware/storage.cpp b/Archive/OMX-27-firmware/src/hardware/storage.cpp
new file mode 100644
index 00000000..2f443311
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/hardware/storage.cpp
@@ -0,0 +1,59 @@
+#include
+#include
+#include
+
+#include "storage.h"
+
+// Storage
+
+Storage *Storage::initStorage()
+{
+ Adafruit_FRAM_I2C fram = Adafruit_FRAM_I2C();
+ // check if FRAM chip can be initialised
+ if (fram.begin())
+ {
+ return new FRAMStorage(fram);
+ }
+ // fall back to EEPROM
+ return new EEPROMStorage();
+}
+
+void Storage::readArray(size_t address, uint8_t buffer[], int length)
+{
+ for (int i = 0; i < length; i++)
+ {
+ buffer[i] = this->read(address + i);
+ }
+}
+
+void Storage::writeArray(size_t address, uint8_t buffer[], int length)
+{
+ for (int i = 0; i < length; i++)
+ {
+ this->write(address + i, buffer[i]);
+ }
+}
+
+// EEPROM
+
+void EEPROMStorage::write(size_t address, uint8_t value)
+{
+ EEPROM.update(address, value);
+}
+
+uint8_t EEPROMStorage::read(size_t address)
+{
+ return EEPROM.read(address);
+}
+
+// FRAM
+
+void FRAMStorage::write(size_t address, uint8_t value)
+{
+ this->fram.write(address, value);
+}
+
+uint8_t FRAMStorage::read(size_t address)
+{
+ return this->fram.read(address);
+}
diff --git a/Archive/OMX-27-firmware/src/hardware/storage.h b/Archive/OMX-27-firmware/src/hardware/storage.h
new file mode 100644
index 00000000..8db2f3b2
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/hardware/storage.h
@@ -0,0 +1,111 @@
+#pragma once
+
+#include
+
+enum StorageType
+{
+ EEPROM_MEMORY = 0,
+ FRAM_MEMORY = 1
+};
+
+// abstract storage class
+class Storage
+{
+public:
+ static Storage *initStorage();
+
+ virtual int capacity() = 0;
+
+ // read/write bytes
+ virtual void write(size_t address, uint8_t val) = 0;
+ virtual uint8_t read(size_t address) = 0;
+ virtual bool isEeprom() = 0;
+
+ void readArray(size_t address, uint8_t buffer[], int length);
+ void writeArray(size_t address, uint8_t buffer[], int length);
+
+ // reset entire storage back to 0
+ void clear()
+ {
+ for (int address = 0; address < capacity(); address++)
+ {
+ write(address, 0);
+ }
+ }
+
+ // template reader/writer implementation copied from Adafruit_FRAM_I2C which implements them both
+ // in terms of reading/writing bytes
+
+ /**************************************************************************/
+ /*!
+ @brief Write any object to memory
+ @param addr
+ The 16-bit address to write to in EEPROM memory
+ @param value The templated object we will be writing
+ @returns The number of bytes written
+ */
+ /**************************************************************************/
+ template
+ uint16_t writeObject(uint16_t addr, const T &value)
+ {
+ const byte *p = (const byte *)(const void *)&value;
+ uint16_t n;
+ for (n = 0; n < sizeof(value); n++)
+ {
+ write(addr++, *p++);
+ }
+ return n;
+ }
+
+ /**************************************************************************/
+ /*!
+ @brief Read any object from memory
+ @param addr
+ The 16-bit address to write to in EEPROM memory
+ @param value The address of the templated object we will be writing INTO
+ @returns The number of bytes read
+ */
+ /**************************************************************************/
+ template
+ uint16_t readObject(uint16_t addr, T &value)
+ {
+ byte *p = (byte *)(void *)&value;
+ uint16_t n;
+ for (n = 0; n < sizeof(value); n++)
+ {
+ *p++ = read(addr++);
+ }
+ return n;
+ }
+
+protected:
+ Storage() {}
+};
+
+class EEPROMStorage : public Storage
+{
+public:
+ EEPROMStorage() {}
+
+ bool isEeprom() override { return true; }
+ void write(size_t address, uint8_t val) override;
+ uint8_t read(size_t address) override;
+ int capacity() override { return 2048; } // 2KB
+};
+
+class FRAMStorage : public Storage
+{
+public:
+ FRAMStorage(Adafruit_FRAM_I2C fram)
+ {
+ this->fram = fram;
+ }
+
+ bool isEeprom() override { return false; }
+ void write(size_t address, uint8_t val) override;
+ uint8_t read(size_t address) override;
+ int capacity() override { return 32000; } // 32KB
+
+private:
+ Adafruit_FRAM_I2C fram;
+};
diff --git a/Archive/OMX-27-firmware/src/midi/midi.cpp b/Archive/OMX-27-firmware/src/midi/midi.cpp
new file mode 100644
index 00000000..841dd483
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/midi/midi.cpp
@@ -0,0 +1,99 @@
+#include "./midi.h"
+#include
+
+namespace
+{
+ using SerialMIDI = midi::SerialMIDI;
+ using MidiInterface = midi::MidiInterface;
+
+ SerialMIDI theSerialInstance(Serial1);
+ MidiInterface HWMIDI(theSerialInstance);
+}
+
+namespace MM
+{
+ void begin()
+ {
+ HWMIDI.begin();
+ }
+
+ void sendNoteOn(int note, int velocity, int channel)
+ {
+ usbMIDI.sendNoteOn(note, velocity, channel);
+ HWMIDI.sendNoteOn(note, velocity, channel);
+ }
+
+ void sendNoteOnHW(int note, int velocity, int channel)
+ {
+ HWMIDI.sendNoteOn(note, velocity, channel);
+ }
+
+ void sendNoteOff(int note, int velocity, int channel)
+ {
+ usbMIDI.sendNoteOff(note, velocity, channel);
+ HWMIDI.sendNoteOff(note, velocity, channel);
+ }
+
+ void sendNoteOffHW(int note, int velocity, int channel)
+ {
+ HWMIDI.sendNoteOff(note, velocity, channel);
+ }
+
+ void sendControlChange(int control, int value, int channel)
+ {
+ usbMIDI.sendControlChange(control, value, channel);
+ HWMIDI.sendControlChange(control, value, channel);
+ }
+
+ void sendControlChangeHW(int control, int value, int channel)
+ {
+ HWMIDI.sendControlChange(control, value, channel);
+ }
+
+ void sendProgramChange(int program, int channel)
+ {
+ usbMIDI.sendProgramChange(program, channel);
+ HWMIDI.sendProgramChange(program, channel);
+ }
+
+ void sendSysEx(uint32_t length, const uint8_t *sysexData, bool hasBeginEnd)
+ {
+ usbMIDI.sendSysEx(length, sysexData, hasBeginEnd);
+ }
+
+ void sendClock()
+ {
+ usbMIDI.sendRealTime(usbMIDI.Clock);
+ HWMIDI.sendClock();
+ }
+
+ void startClock()
+ {
+ usbMIDI.sendRealTime(usbMIDI.Start);
+ HWMIDI.sendStart();
+ }
+
+ void continueClock()
+ {
+ usbMIDI.sendRealTime(usbMIDI.Continue);
+ HWMIDI.sendContinue();
+ }
+
+ void stopClock()
+ {
+ usbMIDI.sendRealTime(usbMIDI.Stop);
+ HWMIDI.sendStop();
+ }
+
+ // NEED SOMETHING FOR usbMIDI.read() / MIDI.read()
+
+ bool usbMidiRead()
+ {
+ return usbMIDI.read();
+ }
+
+ bool midiRead()
+ {
+ return HWMIDI.read();
+ }
+}
diff --git a/Archive/OMX-27-firmware/src/midi/midi.h b/Archive/OMX-27-firmware/src/midi/midi.h
new file mode 100644
index 00000000..12cc9f0a
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/midi/midi.h
@@ -0,0 +1,26 @@
+#pragma once
+
+#include
+
+namespace MM
+{
+
+ void begin();
+
+ void sendNoteOn(int note, int velocity, int channel);
+ void sendNoteOff(int note, int velocity, int channel);
+ void sendControlChange(int control, int value, int channel);
+ void sendProgramChange(int program, int channel);
+ void sendNoteOnHW(int note, int velocity, int channel);
+ void sendNoteOffHW(int note, int velocity, int channel);
+ void sendControlChangeHW(int control, int value, int channel);
+ void sendSysEx(uint32_t length, const uint8_t *sysexData, bool hasBeginEnd);
+
+ void sendClock();
+ void startClock();
+ void continueClock();
+ void stopClock();
+
+ bool usbMidiRead();
+ bool midiRead();
+}
diff --git a/Archive/OMX-27-firmware/src/midi/noteoffs.cpp b/Archive/OMX-27-firmware/src/midi/noteoffs.cpp
new file mode 100644
index 00000000..a503d226
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/midi/noteoffs.cpp
@@ -0,0 +1,279 @@
+#include "noteoffs.h"
+
+#include
+#include "../consts/consts.h"
+#include "../config.h"
+#include "../midi/midi.h"
+#include "../utils/cvNote_util.h"
+
+PendingNoteHistory::PendingNoteHistory()
+{
+ clear();
+}
+
+void PendingNoteHistory::clear()
+{
+ for (int i = 0; i < queueSize; ++i)
+ {
+ queue[i].inUse = false;
+ }
+ prevTime = micros();
+}
+
+void PendingNoteHistory::clearIfChanged(uint32_t time)
+{
+ if (time != prevTime)
+ {
+ clear();
+ }
+}
+
+bool PendingNoteHistory::insert(int note, int channel)
+{
+ for (int i = 0; i < queueSize; ++i)
+ {
+ if (queue[i].inUse)
+ continue;
+ queue[i].inUse = true;
+ queue[i].note = note;
+ queue[i].channel = channel;
+ return true;
+ }
+ return false; // couldn't find room!
+}
+
+bool PendingNoteHistory::eventThisFrame(int note, int channel)
+{
+ for (int i = 0; i < queueSize; ++i)
+ {
+ if (queue[i].inUse)
+ {
+ if (queue[i].note == note && queue[i].channel == channel)
+ return true;
+ }
+ }
+ return false; // couldn't find room!
+}
+
+PendingNoteHistory pendingNoteHistory;
+
+PendingNoteOffs::PendingNoteOffs()
+{
+ for (int i = 0; i < queueSize; ++i)
+ queue[i].inUse = false;
+}
+
+bool PendingNoteOffs::insert(int note, int channel, uint32_t time, bool sendCV)
+{
+ for (int i = 0; i < queueSize; ++i)
+ {
+ if (queue[i].inUse)
+ continue;
+ queue[i].inUse = true;
+ queue[i].note = note;
+ queue[i].time = time;
+ queue[i].channel = channel;
+ queue[i].sendCV = sendCV;
+ return true;
+ }
+ return false; // couldn't find room!
+}
+
+void PendingNoteOffs::play(uint32_t now)
+{
+ for (int i = 0; i < queueSize; ++i)
+ {
+ if (queue[i].inUse && queue[i].time <= now)
+ {
+ MM::sendNoteOff(queue[i].note, 0, queue[i].channel);
+ // analogWrite(CVPITCH_PIN, 0);
+ if (queue[i].sendCV)
+ {
+ cvNoteUtil.cvNoteOff(queue[i].note);
+ }
+ queue[i].inUse = false;
+
+ onNoteOff(queue[i].note, queue[i].channel);
+
+ // if (pendingNoteHistory.eventThisFrame(queue[i].note, queue[i].channel) == false)
+ // {
+ // pendingNoteHistory.insert(queue[i].note, queue[i].channel);
+
+ // MM::sendNoteOff(queue[i].note, 0, queue[i].channel);
+ // // analogWrite(CVPITCH_PIN, 0);
+ // if (queue[i].sendCV)
+ // {
+ // digitalWrite(CVGATE_PIN, LOW);
+ // }
+ // queue[i].inUse = false;
+
+ // onNoteOff(queue[i].note, queue[i].channel);
+ // }
+ // else
+ // {
+ // // queue[i].time += 200;
+ // }
+ }
+ }
+}
+
+bool PendingNoteOffs::sendOffIfPresent(int note, int channel, bool sendCV)
+{
+ bool noteOffSent = false;
+
+ // Find notes in queue matching note number and channel
+ for (int i = 0; i < queueSize; ++i)
+ {
+ if (queue[i].inUse && queue[i].channel == channel && queue[i].note == note)
+ {
+ // Send note off event for first note found
+ // Other pending note offs just get set to not in use.
+ if (!noteOffSent)
+ {
+ pendingNoteHistory.insert(queue[i].note, queue[i].channel);
+ MM::sendNoteOff(queue[i].note, 0, queue[i].channel);
+ // analogWrite(CVPITCH_PIN, 0);
+ if (queue[i].sendCV)
+ {
+ cvNoteUtil.cvNoteOff(queue[i].note);
+ }
+ noteOffSent = true;
+ onNoteOff(queue[i].note, queue[i].channel);
+ }
+ queue[i].inUse = false;
+ }
+ }
+
+ return noteOffSent;
+}
+
+void PendingNoteOffs::sendOffNow(int note, int channel, bool sendCV)
+{
+ bool noteOffSent = sendOffIfPresent(note, channel, sendCV);
+
+ if (!noteOffSent)
+ {
+ pendingNoteHistory.insert(note, channel);
+ MM::sendNoteOff(note, 0, channel);
+ if (sendCV)
+ {
+ cvNoteUtil.cvNoteOff(note);
+ }
+ onNoteOff(note, channel);
+ }
+}
+
+void PendingNoteOffs::allOff()
+{
+ play(UINT32_MAX);
+}
+
+void PendingNoteOffs::setNoteOffFunction(void (*fptr)(void *, int note, int channel), void *context)
+{
+ setNoteOffFuncPtrContext = context;
+ setNoteOffFuncPtr = fptr;
+}
+
+void PendingNoteOffs::onNoteOff(int note, int channel)
+{
+ // Serial.println("PendingNoteOffs::onNoteOff " + String(note) + " " + String(channel));
+ if (setNoteOffFuncPtrContext != nullptr)
+ {
+ // Serial.println("PendingNoteOffs::onNoteOff sending to pointer");
+ setNoteOffFuncPtr(setNoteOffFuncPtrContext, note, channel);
+ }
+ // else{
+ // Serial.println("PendingNoteOffs::onNoteOff pointer not found");
+ // }
+}
+
+PendingNoteOffs pendingNoteOffs;
+
+///
+
+PendingNoteOns::PendingNoteOns()
+{
+ for (int i = 0; i < queueSize; ++i)
+ queue[i].inUse = false;
+}
+
+bool PendingNoteOns::insert(int note, int velocity, int channel, uint32_t time, bool sendCV)
+{
+
+ // pendingNoteOffs.sendOffIfPresent(note, channel, sendCV);
+
+ for (int i = 0; i < queueSize; ++i)
+ {
+ if (queue[i].inUse)
+ continue;
+ queue[i].inUse = true;
+ queue[i].note = note;
+ queue[i].time = time;
+ queue[i].channel = channel;
+ queue[i].velocity = velocity;
+ queue[i].sendCV = sendCV;
+ return true;
+ }
+ return false; // couldn't find room!
+}
+
+bool PendingNoteOns::remove(int note, int channel)
+{
+ bool foundNoteToRemove = false;
+
+ // Find notes in queue matching note number and channel
+ for (int i = 0; i < queueSize; ++i)
+ {
+ if (queue[i].inUse && queue[i].channel == channel && queue[i].note == note)
+ {
+ queue[i].inUse = false;
+ foundNoteToRemove = true;
+ }
+ }
+
+ return foundNoteToRemove;
+}
+
+void PendingNoteOns::play(uint32_t now)
+{
+ // int pCV;
+ for (int i = 0; i < queueSize; ++i)
+ {
+ if (queue[i].inUse && queue[i].time <= now)
+ {
+ midiSettings.midiLastNote = queue[i].note;
+ midiSettings.midiLastVel = queue[i].velocity;
+
+ MM::sendNoteOn(queue[i].note, queue[i].velocity, queue[i].channel);
+
+ if (queue[i].sendCV)
+ {
+ cvNoteUtil.cvNoteOn(queue[i].note);
+ }
+ queue[i].inUse = false;
+
+ // if (pendingNoteHistory.eventThisFrame(queue[i].note, queue[i].channel) == false)
+ // {
+ // pendingNoteHistory.insert(queue[i].note, queue[i].channel);
+ // MM::sendNoteOn(queue[i].note, queue[i].velocity, queue[i].channel);
+
+ // if (queue[i].sendCV)
+ // {
+ // if (queue[i].note >= cvLowestNote && queue[i].note < cvHightestNote)
+ // {
+ // pCV = static_cast(roundf((queue[i].note - cvLowestNote) * stepsPerSemitone));
+ // digitalWrite(CVGATE_PIN, HIGH);
+ // analogWrite(CVPITCH_PIN, pCV);
+ // }
+ // }
+ // queue[i].inUse = false;
+ // }
+ // else
+ // {
+ // // queue[i].time += 200;
+ // }
+ }
+ }
+}
+
+PendingNoteOns pendingNoteOns;
diff --git a/Archive/OMX-27-firmware/src/midi/noteoffs.h b/Archive/OMX-27-firmware/src/midi/noteoffs.h
new file mode 100644
index 00000000..9acc1629
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/midi/noteoffs.h
@@ -0,0 +1,90 @@
+#pragma once
+
+#include
+
+class PendingNoteHistory
+{
+public:
+ PendingNoteHistory();
+ void clear();
+ void clearIfChanged(uint32_t time);
+ bool insert(int note, int channel);
+ bool eventThisFrame(int note, int channel);
+
+private:
+ struct Entry
+ {
+ bool inUse = false;
+ int note : 7;
+ int channel : 5;
+ };
+ static const int queueSize = 32;
+ Entry queue[queueSize];
+
+ uint32_t prevTime;
+};
+
+extern PendingNoteHistory pendingNoteHistory;
+
+class PendingNoteOffs
+{
+public:
+ PendingNoteOffs();
+ bool insert(int note, int channel, uint32_t time, bool sendCV);
+ void play(uint32_t time);
+
+ // Finds any pending note offs for this note and kills them
+ // so they won't later fire
+ // then sends the note off event now
+ bool sendOffIfPresent(int note, int channel, bool sendCV);
+ void sendOffNow(int note, int channel, bool sendCV);
+ void allOff();
+
+ void setNoteOffFunction(void (*fptr)(void *, int note, int channel), void *context);
+
+private:
+ struct Entry
+ {
+ bool inUse;
+ int note;
+ int channel;
+ bool sendCV;
+ uint32_t time;
+ };
+ static const int queueSize = 32;
+ Entry queue[queueSize];
+
+ void onNoteOff(int note, int channel);
+
+ // Pointer to external function that notes are sent out of fxgroup to
+ void *setNoteOffFuncPtrContext = nullptr;
+ void (*setNoteOffFuncPtr)(void *, int note, int channel);
+};
+
+extern PendingNoteOffs pendingNoteOffs;
+
+class PendingNoteOns
+{
+public:
+ PendingNoteOns();
+ bool insert(int note, int velocity, int channel, uint32_t time, bool sendCV);
+
+ // Remove any notes matching description
+ bool remove(int note, int channel);
+ void play(uint32_t time);
+
+private:
+ struct Entry
+ {
+ bool inUse;
+ int note;
+ int channel;
+ int velocity;
+ bool sendCV;
+ uint32_t time;
+ };
+ static const int queueSize = 32;
+ Entry queue[queueSize];
+};
+
+extern PendingNoteOns pendingNoteOns;
diff --git a/Archive/OMX-27-firmware/src/midi/sysex.cpp b/Archive/OMX-27-firmware/src/midi/sysex.cpp
new file mode 100644
index 00000000..4431eb32
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/midi/sysex.cpp
@@ -0,0 +1,146 @@
+#include "../midi/sysex.h"
+#include "../midi/midi.h"
+#include "../config.h"
+
+const uint8_t INFO = 0x1F;
+const uint8_t CONFIG_EDIT = 0x0E;
+const uint8_t CONFIG_DEVICE_EDIT = 0x0D;
+
+void SysEx::processIncomingSysex(const uint8_t *sysexData, unsigned size)
+{
+ if (size < 3)
+ {
+ // Serial.println("That's an empty sysex");
+ return;
+ }
+ // F0 7D 00 00
+ if (!(sysexData[1] == 0x7d && sysexData[2] == 0x00 && sysexData[3] == 0x00))
+ {
+ // Serial.println("Not a valid sysex message for us");
+ return;
+ }
+
+ switch (sysexData[4])
+ {
+ case INFO:
+ // 1F = "1nFo" - please send me your current config
+ // Serial.println("Got an 1nFo request");
+ this->sendCurrentState();
+ break;
+ case CONFIG_EDIT:
+ // 0E - c0nfig Edit - here is a new config
+ // Serial.println("Got an c0nfig Edit");
+ this->updateAllSettingsAndStore(sysexData, size);
+ break;
+ case CONFIG_DEVICE_EDIT:
+ // 0D - c0nfig Device edit - new config just for device opts
+ // Serial.println("Got an c0nfig Device Edit");
+ this->updateDeviceSettingsAndStore(sysexData, size);
+ break;
+ default:
+ break;
+ // case 0x0a:
+ // // 0a - change config, don't store
+ // this->updateDeviceSettings(sysexData, size);
+ // break;
+ // case 0x0c:
+ // // 0C - c0nfig usb edit - here is a new config just for usb
+ // updateUSBSettingsAndStore(sysexData, size);
+ // break;
+ // case 0x0b:
+ // // 0B - c0nfig trs edit - here is a new config just for trs
+ // updateTRSSettingsAndStore(sysexData, size);
+ // break;
+ }
+}
+
+void SysEx::updateAllSettingsAndStore(const uint8_t *newConfig, unsigned size)
+{
+ this->updateSettingsBlockAndStore(newConfig, size, 9, 80, 0);
+}
+
+void SysEx::updateDeviceSettingsAndStore(const uint8_t *newConfig, unsigned size)
+{
+ this->updateSettingsBlockAndStore(newConfig, size, 5, 32, 0);
+}
+
+void SysEx::updateDeviceSettings(const uint8_t *newConfig, unsigned size)
+{
+ // think about this option
+}
+
+void SysEx::updateSettingsBlockAndStore(const uint8_t *configFromSysex, unsigned sysexSize, int configStartIndex, int configDataLength, int EEPROMStartIndex)
+{
+ // walk the config, ignoring the top, tail, and firmware version
+ uint8_t dataToWrite[configDataLength];
+
+ for (int i = 0; i < (configDataLength); i++)
+ {
+ int configIndex = i + configStartIndex;
+ dataToWrite[i] = configFromSysex[configIndex];
+ }
+
+ // write new Data
+ this->storage->writeArray(EEPROMStartIndex, dataToWrite, configDataLength);
+ this->loadGlobals();
+}
+
+void SysEx::loadGlobals(void)
+{
+ // uint8_t version = this->storage->read(EEPROM_HEADER_ADDRESS + 0);
+ this->settings->omxMode = (OMXMode)this->storage->read(EEPROM_HEADER_ADDRESS + 1);
+ this->settings->playingPattern = this->storage->read(EEPROM_HEADER_ADDRESS + 2);
+ uint8_t unMidiChannel = this->storage->read(EEPROM_HEADER_ADDRESS + 3);
+ this->settings->midiChannel = unMidiChannel + 1;
+ for (int b = 0; b < NUM_CC_BANKS; b++)
+ {
+ for (int i = 0; i < NUM_CC_POTS; i++)
+ {
+ pots[b][i] = this->storage->read(EEPROM_HEADER_ADDRESS + 4 + i + (5 * b));
+ }
+ }
+ this->settings->refresh = true;
+}
+
+void SysEx::sendCurrentState()
+{
+ // 0F - "c0nFig" - outputs its config:
+ uint8_t sysexData[EEPROM_HEADER_SIZE + 8];
+
+ sysexData[0] = 0x7d; // manufacturer
+ sysexData[1] = 0x00;
+ sysexData[2] = 0x00;
+
+ sysexData[3] = 0x0F; // ConFig;
+
+ sysexData[4] = DEVICE_ID; // Device 01, ie, dev board
+ sysexData[5] = MAJOR_VERSION; // major version
+ sysexData[6] = MINOR_VERSION; // minor version
+ sysexData[7] = POINT_VERSION; // point version
+
+ // 32 bytes of data:
+ // EEPROM VERSION
+ // MODE
+ // PlayingPattern
+ // MidiChannel
+ // Pots (x25 - 5 banks of 5 pots)
+ // 00
+ // 00
+ // 00
+
+ uint8_t buffer[EEPROM_HEADER_SIZE];
+ this->storage->readArray(0, buffer, EEPROM_HEADER_SIZE);
+
+ int offset = 8;
+ for (int i = 0; i < EEPROM_HEADER_SIZE; i++)
+ {
+ int data = buffer[i];
+ if (data == 0xff)
+ {
+ data = 0x7f;
+ }
+ sysexData[i + offset] = data;
+ }
+
+ MM::sendSysEx(EEPROM_HEADER_SIZE + offset, sysexData, false);
+}
diff --git a/Archive/OMX-27-firmware/src/midi/sysex.h b/Archive/OMX-27-firmware/src/midi/sysex.h
new file mode 100644
index 00000000..5a70be8d
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/midi/sysex.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include "../hardware/storage.h"
+#include "../config.h"
+
+class SysEx
+{
+ Storage *storage;
+ SysSettings *settings;
+
+public:
+ SysEx(Storage *storage, SysSettings *settings) : storage(storage),
+ settings(settings) {}
+
+ void processIncomingSysex(const uint8_t *sysexData, unsigned size);
+ void updateAllSettingsAndStore(const uint8_t *newConfig, unsigned size);
+ void updateDeviceSettingsAndStore(const uint8_t *newConfig, unsigned size);
+ void updateDeviceSettings(const uint8_t *newConfig, unsigned size);
+ void updateSettingsBlockAndStore(const uint8_t *configFromSysex, unsigned sysexSize, int configStartIndex, int configDataLength, int EEPROMStartIndex);
+ void loadGlobals();
+ void sendCurrentState();
+};
diff --git a/Archive/OMX-27-firmware/src/midifx/midifx_arpeggiator.cpp b/Archive/OMX-27-firmware/src/midifx/midifx_arpeggiator.cpp
new file mode 100644
index 00000000..63d37014
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/midifx/midifx_arpeggiator.cpp
@@ -0,0 +1,2621 @@
+#include "midifx_arpeggiator.h"
+#include "../hardware/omx_disp.h"
+#include "../utils/omx_util.h"
+#include "../hardware/omx_leds.h"
+#include "../consts/colors.h"
+// #include "../sequencer.h"
+#include
+// #include
+
+namespace midifx
+{
+ enum ArpPage
+ {
+ ARPPAGE_Chance,
+ ARPPAGE_1,
+ ARPPAGE_2,
+ ARPPAGE_3, // TransposeSteps, TransposeDistance
+ ARPPAGE_4,
+ ARPPAGE_MODPAT,
+ ARPPAGE_TRANSPPAT
+ };
+
+ const char *kModeDisp_[] = {"OFF", "ON", "1-ST", "ONCE", "HOLD"};
+
+ const char *kPatMsg_[] = {
+ "Up",
+ "Down",
+ "UpDown",
+ "DownUp",
+ "Up & Down",
+ "Down & Up",
+ "Converge",
+ "Diverge",
+ "Con-Div",
+ "Hi-Up",
+ "Hi-UpDown",
+ "Low-Up",
+ "Low-UpDown",
+ "Random",
+ "Rand Other",
+ "Rand Once",
+ "As Played"};
+
+ const char *kResetMsg_[] = {
+ "Normal",
+ "Note",
+ "Mod Pat",
+ "Transp Pat",
+ };
+
+ const char *kResetDisp_[] = {
+ "NORM",
+ "NOTE",
+ "MPAT",
+ "TPAT",
+ };
+
+ const char *kPatDisp_[] = {
+ "UP",
+ "DN",
+ "UPDN",
+ "DNUP",
+ "U&D",
+ "D&U",
+ "CON",
+ "DIV",
+ "C-V",
+ "HI 1",
+ "HI 2",
+ "LO 1",
+ "LO 2",
+ "RAND",
+ "ROTH",
+ "RONC",
+ "ASP"};
+
+ const char *kArpModDisp_[] = {
+ "×",
+ ".",
+ "-",
+ "R",
+ "<",
+ ">",
+ "\"",
+ "#",
+ "1",
+ "2",
+ "3",
+ "4",
+ "5",
+ "6"};
+
+ const char *kArpModMsg_[] = {
+ "As Played",
+ "Rest",
+ "Tie",
+ "Repeat",
+ "LoPitch -Oct",
+ "HiPitch +Oct",
+ "PwrChord",
+ "Chord",
+ "Note 1",
+ "Note 2",
+ "Note 3",
+ "Note 4",
+ "Note 5",
+ "Note 6"};
+
+ MidiFXArpeggiator::MidiFXArpeggiator()
+ {
+ chancePerc_ = 100;
+ arpMode_ = 0;
+ resetMode_ = ARPRESET_NORMAL;
+ arpPattern_ = 0;
+ midiChannel_ = 0;
+ swing_ = 0;
+ rateIndex_ = 6;
+ octaveRange_ = 1; // 2 Octaves
+ octDistance_ = 12; // 12 notes per oct
+ modPatternLength_ = 15;
+ transpPatternLength_ = 15;
+ syncPos_ = 0;
+
+ quantizedRateIndex_ = -1; // Use global
+ quantizeSync_ = quantizedRateIndex_ >= -1;
+
+ heldKey16_ = -1;
+
+ prevArpMode_ = 0;
+
+ changeArpMode(arpMode_);
+
+ params_.addPage(1);
+ params_.addPage(4);
+ params_.addPage(4);
+ params_.addPage(4);
+ params_.addPage(4);
+ params_.addPage(17);
+ params_.addPage(17);
+
+ encoderSelect_ = true;
+
+ for (uint8_t i = 0; i < 16; i++)
+ {
+ modPattern_[i].mod = MODPAT_ARPNOTE;
+ transpPattern_[i] = 0;
+
+ // if(i % 2 == 0)
+ // {
+ // modPattern_[i].mod = MODPAT_ARPNOTE;
+ // }
+ // else
+ // {
+ // modPattern_[i].mod = MODPAT_REST;
+ // }
+ }
+
+ noteMaster.setContext(this);
+ noteMaster.setProcessNoteFptr(&processNoteForwarder);
+ noteMaster.setSendNoteOutFptr(&sendNoteOutForwarder);
+
+ // for (uint8_t i = 0; i < 8; i++)
+ // {
+ // trackingNoteGroups[i].prevNoteNumber = 255;
+ // trackingNoteGroupsPassthrough[i].prevNoteNumber = 255;
+ // }
+ }
+
+ MidiFXArpeggiator::~MidiFXArpeggiator()
+ {
+ if (arpRunning_)
+ {
+ // Remove from this
+ seqConfig.numOfActiveArps--;
+ }
+ }
+
+ int MidiFXArpeggiator::getFXType()
+ {
+ return MIDIFX_ARP;
+ }
+
+ const char *MidiFXArpeggiator::getName()
+ {
+ return "Arp";
+ }
+
+ const char *MidiFXArpeggiator::getDispName()
+ {
+ return "ARP";
+ }
+
+ MidiFXInterface *MidiFXArpeggiator::getClone()
+ {
+ MidiFXArpeggiator *clone = new MidiFXArpeggiator();
+ clone->chancePerc_ = chancePerc_;
+ clone->arpMode_ = arpMode_;
+ clone->arpPattern_ = arpPattern_;
+ clone->resetMode_ = resetMode_;
+ clone->midiChannel_ = midiChannel_;
+ clone->swing_ = swing_;
+ clone->rateIndex_ = rateIndex_;
+ clone->quantizedRateIndex_ = rateIndex_;
+ clone->octaveRange_ = octaveRange_;
+ clone->octDistance_ = octDistance_;
+ clone->gate = gate;
+ clone->modPatternLength_ = modPatternLength_;
+ clone->transpPatternLength_ = transpPatternLength_;
+ clone->multiplierCalculated_ = false;
+ clone->quantizeSync_ = quantizedRateIndex_ >= -1;
+
+ for (uint8_t i = 0; i < 16; i++)
+ {
+ clone->modPattern_[i] = modPattern_[i];
+ clone->transpPattern_[i] = transpPattern_[i];
+ }
+
+ clone->changeArpMode(arpMode_);
+
+ return clone;
+ }
+
+ // Toggles between off and previous mode
+ void MidiFXArpeggiator::toggleArp()
+ {
+ if (prevArpMode_ == ARPMODE_OFF)
+ {
+ prevArpMode_ = ARPMODE_ON;
+ }
+
+ if (arpMode_ == ARPMODE_OFF)
+ {
+ changeArpMode(prevArpMode_);
+ }
+ else
+ {
+ prevArpMode_ = arpMode_;
+ changeArpMode(ARPMODE_OFF);
+ }
+ }
+
+ void MidiFXArpeggiator::toggleHold()
+ {
+ // Serial.println("Prev Arp Mode: " + String(prevArpMode_));
+ // Serial.println("Arp Mode: " + String(arpMode_));
+
+ if (arpMode_ == ARPMODE_OFF)
+ {
+ if (prevArpMode_ == ARPMODE_HOLD)
+ {
+ prevArpMode_ = ARPMODE_ON;
+ }
+ else
+ {
+ prevArpMode_ = ARPMODE_HOLD;
+ }
+ }
+ else
+ {
+ if (arpMode_ == ARPMODE_HOLD)
+ {
+ if (prevArpMode_ == ARPMODE_HOLD)
+ {
+ changeArpMode(ARPMODE_ON);
+ }
+ else
+ {
+ changeArpMode(prevArpMode_);
+ }
+ prevArpMode_ = ARPMODE_HOLD;
+ }
+ else
+ {
+ prevArpMode_ = arpMode_;
+ changeArpMode(ARPMODE_HOLD);
+ }
+ }
+ }
+
+ void MidiFXArpeggiator::nextArpPattern()
+ {
+ arpPattern_ = (arpPattern_ + 1) % ARPPAT_NUM_OF_PATS;
+ omxDisp.displayMessage(kPatMsg_[arpPattern_]);
+ sortNotes();
+ }
+
+ void MidiFXArpeggiator::nextOctRange()
+ {
+ octaveRange_ = (octaveRange_ + 1) % 4;
+
+ omxDisp.displayMessageTimed("OctRange: " + String(octaveRange_ + 1), 5);
+ }
+
+ bool MidiFXArpeggiator::isOn()
+ {
+ return arpMode_ != ARPMODE_OFF;
+ }
+ bool MidiFXArpeggiator::isHoldOn()
+ {
+ if (arpMode_ == ARPMODE_OFF)
+ {
+ return isModeHold(prevArpMode_);
+ }
+ else
+ {
+ return isModeHold(arpMode_);
+ }
+ }
+
+ uint8_t MidiFXArpeggiator::getOctaveRange()
+ {
+ return octaveRange_;
+ }
+
+ bool MidiFXArpeggiator::isModeHold(uint8_t arpMode)
+ {
+ switch (arpMode)
+ {
+ case ARPMODE_OFF:
+ case ARPMODE_ON:
+ case ARPMODE_ONESHOT:
+ case ARPMODE_ONCE:
+ return false;
+ case ARPMODE_HOLD:
+ return true;
+ }
+
+ return false;
+ }
+
+ void MidiFXArpeggiator::changeArpMode(uint8_t newArpMode)
+ {
+ arpMode_ = newArpMode;
+
+ if ((arpMode_ == ARPMODE_ON && hasMidiNotes() == false) || (arpMode_ == ARPMODE_ONCE && hasMidiNotes() == false) || arpMode_ == ARPMODE_OFF)
+ {
+ stopArp();
+ }
+
+ switch (arpMode_)
+ {
+ case ARPMODE_OFF:
+ case ARPMODE_ON:
+ resync();
+ break;
+ }
+ }
+
+ void MidiFXArpeggiator::onModeChanged()
+ {
+ stopArp();
+ playedNoteQueue.clear();
+ holdNoteQueue.clear();
+ sortedNoteQueue.clear();
+ tempNoteQueue.clear();
+ fixedLengthNotes.clear();
+ heldKey16_ = -1;
+ }
+
+ void MidiFXArpeggiator::onEnabled()
+ {
+ heldKey16_ = -1;
+ // stopArp();
+ // playedNoteQueue.clear();
+ // holdNoteQueue.clear();
+ // sortedNoteQueue.clear();
+ }
+
+ void MidiFXArpeggiator::onDisabled()
+ {
+ // stopArp();
+ // playedNoteQueue.clear();
+ // holdNoteQueue.clear();
+ // sortedNoteQueue.clear();
+ }
+
+ void MidiFXArpeggiator::onSelected()
+ {
+ if (sysSettings.omxMode == MODE_MIDI && arpRunning_)
+ {
+ resetArpSeq();
+ startArp();
+ }
+ }
+
+ void MidiFXArpeggiator::onDeselected()
+ {
+ }
+
+ void MidiFXArpeggiator::noteInput(MidiNoteGroup note)
+ {
+ if (arpMode_ == ARPMODE_OFF)
+ {
+ sendNoteOut(note);
+ return;
+ }
+
+ // if(arpRunning_ && note.channel != (midiChannel_ + 1))
+ // {
+ // sendNoteOut(note);
+ // return;
+ // }
+
+ if (chancePerc_ != 100 && (chancePerc_ == 0 || random(100) > chancePerc_))
+ {
+ noteMaster.trackNoteInputPassthrough(¬e);
+ // // sendNoteOut(note);
+ // if(note.unknownLength || note.noteOff)
+ // {
+ // trackNoteInputPassthrough(note, false);
+ // }
+ // else
+ // {
+ // sendNoteOut(note);
+ // }
+
+ return;
+ }
+
+ noteMaster.trackNoteInput(¬e);
+
+ // if (note.unknownLength || note.noteOff)
+ // {
+ // // only notes of unknown lengths need to be tracked
+ // // notes with fixed lengths will turn off automatically.
+ // trackNoteInputPassthrough(note, true);
+ // trackNoteInput(note);
+ // }
+ // else
+ // {
+ // processNoteInput(note);
+ // }
+ }
+
+ // If chance is less than 100% and passing through, notes need to be tracked
+ // and if the same note comes in without passthrough for a noteoff event, it needs to
+ // be passed through to send noteoff to prevent stuck notes
+ // void MidiFXArpeggiator::trackNoteInputPassthrough(MidiNoteGroup note, bool ignoreNoteOns)
+ // {
+ // // Note on, not ignored
+ // if (!ignoreNoteOns && !note.noteOff)
+ // {
+ // // Search for an empty slot in trackingNoteGroupsPassthrough
+ // // If no slots are available/more than 8 notes/ note gets killed.
+ // for (uint8_t i = 0; i < 8; i++)
+ // {
+ // // Found empty slot
+ // if (trackingNoteGroupsPassthrough[i].prevNoteNumber == 255)
+ // {
+ // trackingNoteGroupsPassthrough[i].channel = note.channel;
+ // trackingNoteGroupsPassthrough[i].prevNoteNumber = note.prevNoteNumber;
+ // trackingNoteGroupsPassthrough[i].noteNumber = note.noteNumber;
+
+ // // Send it forward through chain
+ // sendNoteOut(note);
+ // return;
+ // }
+ // }
+ // }
+
+ // // Note off
+ // if (note.noteOff)
+ // {
+ // // bool noteFound = false;
+
+ // // Search to see if this note is in trackingNoteGroupsPassthrough
+ // // Meaning it was previously passed through
+ // // If it is found, send it through chain
+ // // PrevNoteNumber should be the origin note number before being modified by MidiFX
+ // for (uint8_t i = 0; i < 8; i++)
+ // {
+ // if (trackingNoteGroupsPassthrough[i].prevNoteNumber != 255)
+ // {
+ // if (trackingNoteGroupsPassthrough[i].channel == note.channel && trackingNoteGroupsPassthrough[i].prevNoteNumber == note.prevNoteNumber)
+ // {
+ // note.noteNumber = trackingNoteGroupsPassthrough[i].noteNumber;
+ // // processNoteInput(note);
+ // sendNoteOut(note);
+ // trackingNoteGroupsPassthrough[i].prevNoteNumber = 255; // mark empty
+ // // noteFound = true;
+ // }
+ // }
+ // }
+
+ // // Should be false if note getting sent to arp
+ // // Avoid double trackNoteInput call
+ // if(!ignoreNoteOns)
+ // {
+ // trackNoteInput(note);
+ // }
+
+ // // Note not previously passed through and is noteoff, now send to arp to turn off arp notes
+ // // if(!noteFound)
+ // // {
+ // // trackNoteInput(note);
+ // // }
+ // }
+ // }
+
+ // void MidiFXArpeggiator::trackNoteInput(MidiNoteGroup note)
+ // {
+ // // Same implementation with more comments in submode_midifx
+ // // Keeps track of previous note ons and and adjusts note number
+ // // for note offs using the prevNoteNumber parameter.
+ // // Why is this necessary?
+ // // If the note is modified by midifx like randomize before the arp
+ // // Then the arp can end up having notes stuck on
+ // // This ensures that notes don't get stuck on.
+ // if (note.noteOff)
+ // {
+ // bool noteFound = false;
+
+ // for (uint8_t i = 0; i < 8; i++)
+ // {
+ // if (trackingNoteGroups[i].prevNoteNumber != 255)
+ // {
+ // if (trackingNoteGroups[i].channel == note.channel && trackingNoteGroups[i].prevNoteNumber == note.prevNoteNumber)
+ // {
+ // note.noteNumber = trackingNoteGroups[i].noteNumber;
+ // processNoteInput(note);
+ // trackingNoteGroups[i].prevNoteNumber = 255; // mark empty
+ // noteFound = true;
+ // }
+ // }
+ // }
+
+ // if (!noteFound)
+ // {
+ // processNoteInput(note);
+ // }
+ // }
+ // else if (!note.noteOff)
+ // {
+ // for (uint8_t i = 0; i < 8; i++)
+ // {
+ // if (trackingNoteGroups[i].prevNoteNumber == 255)
+ // {
+ // trackingNoteGroups[i].channel = note.channel;
+ // trackingNoteGroups[i].prevNoteNumber = note.prevNoteNumber;
+ // trackingNoteGroups[i].noteNumber = note.noteNumber;
+
+ // processNoteInput(note);
+ // return;
+ // }
+ // }
+ // }
+ // }
+
+ void MidiFXArpeggiator::processNoteInput(MidiNoteGroup *note)
+ {
+ if (note->unknownLength)
+ {
+ if (note->noteOff)
+ {
+ arpNoteOff(note);
+ }
+ else
+ {
+ arpNoteOn(note);
+ }
+ }
+ else
+ {
+ bool canInsert = true;
+
+ if (fixedLengthNotes.size() < queueSize)
+ {
+ for (uint8_t i = 0; i < fixedLengthNotes.size(); i++)
+ {
+ PendingArpNote p = fixedLengthNotes[i];
+
+ // Note already exists
+ if (p.noteCache.noteNumber == note->noteNumber && p.noteCache.channel == note->channel)
+ {
+ // Update note off time
+ fixedLengthNotes[i].offTime = seqConfig.currentFrameMicros + (note->stepLength * clockConfig.step_micros);
+ canInsert = false;
+ break;
+ }
+ }
+ }
+ else
+ {
+ canInsert = false;
+ }
+
+ if (canInsert)
+ {
+ // Serial.println("Inserting pending note");
+ PendingArpNote fixedLengthNote;
+ fixedLengthNote.noteCache.setFromNoteGroup(note);
+ fixedLengthNote.offTime = seqConfig.currentFrameMicros + (note->stepLength * clockConfig.step_micros);
+ fixedLengthNotes.push_back(fixedLengthNote);
+ arpNoteOn(note);
+ }
+ else
+ {
+ // Remove from tracking notes
+ noteMaster.removeFromTracking(note);
+ // for (uint8_t i = 0; i < 8; i++)
+ // {
+ // if (trackingNoteGroups[i].prevNoteNumber != 255)
+ // {
+ // if (trackingNoteGroups[i].channel == note->channel && trackingNoteGroups[i].prevNoteNumber == note->prevNoteNumber)
+ // {
+ // trackingNoteGroups[i].prevNoteNumber = 255; // mark empty
+ // }
+ // }
+ // }
+ // Kill note
+ // sendNoteOut(note);
+ }
+ // if(fixedLengthNotes.si)
+ // arpNoteOn(note);
+ }
+ }
+
+ void MidiFXArpeggiator::startArp()
+ {
+ // Serial.println("startArp");
+ if (arpRunning_ || pendingStart_)
+ return;
+
+ pendingStart_ = true;
+ sortOrderChanged_ = false;
+ resetNextTrigger_ = false;
+
+ // pendingStartTime_ = micros();
+
+ notePos_ = 0;
+ prevNotePos_ = 0;
+ nextNotePos_ = 0;
+
+ if(quantizeSync_ == false)
+ {
+ doPendingStart();
+ }
+
+ // if (omxUtil.areClocksRunning() == false)
+ // {
+ // pendingStart_ = true;
+ // }
+ // else
+ // {
+ // doPendingStart();
+ // }
+
+ // if((pendingStartTime_ - seqConfig.lastClockMicros <= 1000) || sysSettings.omxMode == MODE_EUCLID)
+ // {
+ // doPendingStart();
+ // }
+
+ // if(seqConfig.currentFrameMicros - seqConfig.lastClockMicros < 300)
+ // {
+ // onClockTick();
+ // }
+ }
+
+ void MidiFXArpeggiator::doPendingStart()
+ {
+ // Serial.println("doPendingStart()");
+
+ // stepMicroDelta_ = (clockConfig.step_micros * 16) * multiplier_;
+ multiplierCalculated_ = false;
+ nextArpTriggerTime_ = seqConfig.lastClockMicros;
+
+ // if (omxUtil.areClocksRunning() == false)
+ // {
+ // omxUtil.restartClocks();
+ // omxUtil.startClocks();
+ // stepMicroDelta_ = (clockConfig.step_micros * 16) * multiplier_;
+ // nextStepTimeP_ = seqConfig.lastClockMicros; // Should be current time, start now.
+ // nextArpTriggerTime_ = nextStepTimeP_;
+ // }
+ // else
+ // {
+ // nextStepTimeP_ = nextArpTriggerTime_;
+
+ // // stepMicroDelta_ = (clockConfig.step_micros * 16) * multiplier_;
+
+ // // nextStepTimeP_ = seqConfig.lastClockMicros + stepMicroDelta_;
+
+ // // // Add microstep time until nextStep time is in the future
+ // // while(nextStepTimeP_ < seqConfig.currentFrameMicros)
+ // // {
+ // // nextStepTimeP_ += stepMicroDelta_;
+ // // }
+ // // // if next step will be in the past
+ // // if (seqConfig.lastClockMicros + stepMicroDelta_ < seqConfig.currentFrameMicros)
+ // // {
+ // // return; // return and do on next clock.
+
+ // // // In the past, do it after next clock
+ // // // uint32_t nextClockTime = seqConfig.lastClockMicros + (clockConfig.ppqInterval * (PPQ / 24));
+ // // // nextStepTimeP_ = nextClockTime + stepMicroDelta_;
+ // // }
+ // }
+
+ // // tickCount_ = 0;
+ // // patPos_ = 0;
+ // // notePos_ = 0;
+ // // octavePos_ = 0;
+
+ // // resetArpSeq();
+
+ // lastStepTimeP_ = nextStepTimeP_;
+ // startMicros = micros();
+
+ arpRunning_ = true;
+ pendingStart_ = false;
+ pendingStop_ = false;
+
+ seqConfig.numOfActiveArps++;
+
+ // Serial.println("numOfActiveArps: " + String(seqConfig.numOfActiveArps));
+ }
+
+ void MidiFXArpeggiator::stopArp()
+ {
+ pendingStart_ = false;
+ pendingStop_ = true;
+ // arpRunning_ = false;
+ // pendingStopCount_ = 0;
+
+ // doPendingStop();
+
+ // // Serial.println("stopArp");
+ // arpRunning_ = false;
+ // pendingStart_ = false;
+ }
+
+ void MidiFXArpeggiator::doPendingStop()
+ {
+ // Serial.println("doPendingStop");
+ if (arpRunning_)
+ {
+ // Stop clocks if last arp
+ seqConfig.numOfActiveArps--;
+ if (seqConfig.numOfActiveArps <= 0)
+ {
+ // omxUtil.stopClocks();
+ }
+ }
+
+ arpRunning_ = false;
+ pendingStart_ = false;
+ pendingStop_ = false;
+
+ // Serial.println("numOfActiveArps: " + String(seqConfig.numOfActiveArps));
+ }
+
+ bool MidiFXArpeggiator::insertMidiNoteQueue(MidiNoteGroup *note)
+ {
+ // Serial.println("playedNoteQueue capacity: " + String(playedNoteQueue.capacity()));
+ if (playedNoteQueue.capacity() > queueSize)
+ {
+ playedNoteQueue.shrink_to_fit();
+ }
+ if (holdNoteQueue.capacity() > queueSize)
+ {
+ holdNoteQueue.shrink_to_fit();
+ }
+
+ bool noteAdded = false;
+
+ if (playedNoteQueue.size() < queueSize)
+ {
+ playedNoteQueue.push_back(ArpNote(note));
+ noteAdded = true;
+ }
+
+ if (holdNoteQueue.size() < queueSize)
+ {
+ holdNoteQueue.push_back(ArpNote(note));
+ noteAdded = true;
+ }
+
+ // for (int i = 0; i < queueSize; ++i)
+ // {
+ // if (playedNoteQueue[i].inUse)
+ // continue;
+
+ // playedNoteQueue[i].inUse = true;
+ // playedNoteQueue[i].noteNumber = note.noteNumber;
+ // playedNoteQueue[i].velocity = note.velocity;
+ // playedNoteQueue[i].sendMidi = note.sendMidi;
+ // playedNoteQueue[i].sendCV = note.sendCV;
+ // return true;
+ // }
+ // return false; // couldn't find room!
+ return noteAdded;
+ }
+
+ bool MidiFXArpeggiator::removeMidiNoteQueue(MidiNoteGroup *note)
+ {
+ bool foundNoteToRemove = false;
+ auto it = playedNoteQueue.begin();
+ while (it != playedNoteQueue.end())
+ {
+ // remove matching note numbers
+ if (it->noteNumber == note->noteNumber && it->channel == note->channel - 1)
+ {
+ // `erase()` invalidates the iterator, use returned iterator
+ it = playedNoteQueue.erase(it);
+ foundNoteToRemove = true;
+ }
+ else
+ {
+ ++it;
+ }
+ }
+
+ return foundNoteToRemove;
+ }
+
+ bool MidiFXArpeggiator::hasMidiNotes()
+ {
+ return playedNoteQueue.size() > 0;
+ }
+
+ // Copies notes from played note queue and sorts them
+ void MidiFXArpeggiator::sortNotes()
+ {
+ sortedNoteQueue.clear();
+
+ // Copy played or held notes to sorted note queue
+ if (arpMode_ != ARPMODE_ON && arpMode_ != ARPMODE_ONCE)
+ {
+ for (ArpNote a : holdNoteQueue)
+ {
+ sortedNoteQueue.push_back(a);
+ }
+ }
+ else
+ {
+ for (ArpNote a : playedNoteQueue)
+ {
+ sortedNoteQueue.push_back(a);
+ }
+ }
+
+ // Sort low to high.
+ if (arpPattern_ != ARPPAT_AS_PLAYED)
+ {
+ std::sort(sortedNoteQueue.begin(), sortedNoteQueue.end(), compareArpNote);
+ }
+
+ if (sortedNoteQueue.size() > 0)
+ {
+ // Find highest and lowest pitch to use for mod pattern
+ lowestPitch_ = sortedNoteQueue[0].noteNumber;
+ highestPitch_ = sortedNoteQueue[sortedNoteQueue.size() - 1].noteNumber;
+
+ // add an octave to highest if there's only 1 note.
+ if (highestPitch_ == lowestPitch_)
+ {
+ highestPitch_ = lowestPitch_ + 12;
+ }
+ }
+ else
+ {
+ lowestPitch_ = -127;
+ highestPitch_ = -127;
+ }
+
+ if (sortedNoteQueue.size() == 0)
+ return; // Not much to do without any notes
+
+ // Serial.println("sortedNoteQueue capacity: " + String(sortedNoteQueue.capacity()));
+
+ // Alternate sorted with upper high note or lower note.
+ if (arpPattern_ == ARPPAT_HI_UP || arpPattern_ == ARPPAT_HI_UP_DOWN || arpPattern_ == ARPPAT_LOW_UP || arpPattern_ == ARPPAT_LOW_UP_DOWN)
+ {
+ tempNoteQueue.clear();
+
+ auto rootNote = sortedNoteQueue[sortedNoteQueue.size() - 1]; // High note
+
+ if (arpPattern_ == ARPPAT_LOW_UP || arpPattern_ == ARPPAT_LOW_UP_DOWN)
+ {
+ rootNote = sortedNoteQueue[0]; // Low note
+ }
+ // CEGB
+ // BCBEBG-BE-BCBEBG // on updown, down will need to end at index 2
+
+ for (uint8_t i = 0; i < sortedNoteQueue.size(); i++)
+ {
+ auto note = sortedNoteQueue[i];
+
+ // add root than note if note is not the base
+ if (note.noteNumber != rootNote.noteNumber)
+ {
+ tempNoteQueue.push_back(rootNote);
+ tempNoteQueue.push_back(note);
+ }
+ }
+
+ if (tempNoteQueue.size() == 0)
+ {
+ tempNoteQueue.push_back(rootNote);
+ }
+
+ sortedNoteQueue.clear();
+
+ for (ArpNote a : tempNoteQueue)
+ {
+ sortedNoteQueue.push_back(a);
+ }
+ }
+
+ // Randomize notes, playing each note in sorted list only once
+ if (arpPattern_ == ARPPAT_RAND_ONCE)
+ {
+ tempNoteQueue.clear();
+
+ int queueSize = sortedNoteQueue.size();
+
+ for (uint8_t i = 0; i < queueSize; i++)
+ {
+ int randIndex = rand() % sortedNoteQueue.size();
+
+ auto note = sortedNoteQueue[randIndex];
+ tempNoteQueue.push_back(note); // Store in temp
+
+ sortedNoteQueue.erase(sortedNoteQueue.begin() + randIndex); // Remove note from sorted
+ }
+
+ // Put temp back in sorted
+ sortedNoteQueue.clear();
+
+ for (ArpNote a : tempNoteQueue)
+ {
+ sortedNoteQueue.push_back(a);
+ }
+ }
+
+ // Alternate pattern converging in center
+ if (arpPattern_ == ARPPAT_CONVERGE || arpPattern_ == ARPPAT_CONVERGE_DIVERGE || arpPattern_ == ARPPAT_DIVERGE)
+ {
+ uint8_t front = 0;
+ uint8_t back = sortedNoteQueue.size() - 1;
+
+ tempNoteQueue.clear();
+ for (uint8_t i = 0; i < sortedNoteQueue.size(); i++)
+ {
+ uint8_t noteIndex = 0;
+
+ // c,e,g,b,d
+ // c,d,e,b,g
+
+ if (i % 2 == 0)
+ {
+ noteIndex = front;
+ front++;
+ }
+ else
+ {
+ noteIndex = back;
+ back--;
+ }
+
+ tempNoteQueue.push_back(sortedNoteQueue[noteIndex]);
+ }
+
+ sortedNoteQueue.clear();
+
+ for (ArpNote a : tempNoteQueue)
+ {
+ sortedNoteQueue.push_back(a);
+ }
+ }
+
+ // Flip pattern
+ if (arpPattern_ == ARPPAT_DOWN || arpPattern_ == ARPPAT_DOWN_AND_UP || arpPattern_ == ARPPAT_DOWN_UP || arpPattern_ == ARPPAT_DIVERGE)
+ {
+ tempNoteQueue.clear();
+ for (ArpNote a : sortedNoteQueue)
+ {
+ tempNoteQueue.push_back(a);
+ }
+
+ sortedNoteQueue.clear();
+
+ for (int8_t i = tempNoteQueue.size() - 1; i >= 0; i--)
+ {
+ sortedNoteQueue.push_back(tempNoteQueue[i]);
+ }
+
+ // auto it = tempNoteQueue.end();
+ // while (it != tempNoteQueue.begin())
+ // {
+ // auto note = *it;
+ // sortedNoteQueue.push_back(note);
+ // it--;
+ // }
+ }
+
+ // Keep vectors in check
+ if (sortedNoteQueue.capacity() > queueSize)
+ {
+ sortedNoteQueue.shrink_to_fit();
+ }
+
+ if (tempNoteQueue.capacity() > queueSize)
+ {
+ tempNoteQueue.shrink_to_fit();
+ }
+ }
+
+ // void MidiFXArpeggiator::generatePattern()
+ // {
+ // int index = 0;
+
+ // Serial.print("Pat: ");
+
+ // for(uint8_t o = 0; o < (octaveRange_ + 1); o++)
+ // {
+ // for(uint8_t n = 0; n < sortedNoteQueue.size(); n++)
+ // {
+ // notePat_[index].noteNumber = sortedNoteQueue[n].noteNumber + (12 * o);
+ // Serial.print(notePat_[index].noteNumber);
+ // Serial.print(" ");
+ // index++;
+ // }
+ // }
+
+ // Serial.print("\n");
+
+ // notePatLength_ = index;
+
+ // Serial.print("Length: ");
+ // Serial.print(notePatLength_);
+ // Serial.print("\n\n");
+ // }
+
+ void MidiFXArpeggiator::arpNoteOn(MidiNoteGroup *note)
+ {
+ // if(arpMode_ != ARPMODE_ONESHOT && !arpRunning_ )
+ // {
+ // startArp();
+ // }
+
+ // if(arpMode_ == ARPMODE_ONESHOT && !arpRunning_)
+ // {
+ // startArp();
+ // }
+ bool arpReset = false;
+
+ if (!arpRunning_)
+ {
+ startArp();
+ resetArpSeq();
+ arpReset = true;
+ }
+
+ if (hasMidiNotes() == false)
+ {
+ velocity_ = note->velocity;
+ sendMidi_ = note->sendMidi;
+ sendCV_ = note->sendCV;
+ midiChannel_ = note->channel - 1; // note.channel is 1-16, sub 1 for 0-15
+
+ // if(arpMode_ == ARPMODE_ON || arpMode_ == ARPMODE_ONCE)
+ // {
+ // resetArpSeq();
+ // arpReset = true;
+ // }
+ // else if(resetMode_ == ARPRESET_NOTE)
+ // {
+ // resetArpSeq();
+ // arpReset = true;
+ // }
+
+ // if (arpMode_ != ARPMODE_HOLD)
+ // {
+ // resetArpSeq();
+ // // resetNextTrigger_ = true;
+ // arpReset = true;
+ // }
+
+ resetArpSeq();
+ // resetNextTrigger_ = true;
+ arpReset = true;
+
+ holdNoteQueue.clear();
+
+ // if(arpMode_ == ARPMODE_ONESHOT) // Only start when no notes for oneshot
+ // {
+ // startArp();
+ // }
+ }
+ else
+ {
+ if (resetMode_ == ARPRESET_NOTE)
+ {
+ // resetNextTrigger_ = true;
+ resetArpSeq();
+ arpReset = true;
+ }
+ }
+
+ insertMidiNoteQueue(note);
+ sortNotes();
+
+ // generatePattern();
+
+ if (arpReset)
+ {
+ nextNotePos_ = notePos_;
+ prevQLength_ = sortedNoteQueue.size();
+ }
+
+ if (pendingStop_)
+ {
+ pendingStop_ = false;
+ }
+
+ if (!arpReset && !pendingStart_)
+ {
+ // sortOrderChanged_ = true;
+ findIndexOfNextNotePos();
+ }
+ }
+
+ void MidiFXArpeggiator::arpNoteOff(MidiNoteGroup *note)
+ {
+ removeMidiNoteQueue(note);
+
+ sortNotes();
+ // generatePattern();
+
+ if ((arpMode_ == ARPMODE_ON || arpMode_ == ARPMODE_ONCE) && hasMidiNotes() == false)
+ {
+ stopArp();
+ }
+ if (hasMidiNotes())
+ {
+ // sortOrderChanged_ = true;
+ findIndexOfNextNotePos();
+ }
+ }
+
+ // compares the sortedNoteQueue from prev arp trigger
+ // to the current note queue.
+ // Looks for the next note in pattern that matches between two
+ // and sets noteIndex_ to the index of that note.
+ // Idea is to keep arp moving in expected way even when the
+ // held notes change.
+ void MidiFXArpeggiator::findIndexOfNextNotePos()
+ {
+ int prevSize = prevSortedNoteQueue.size();
+ // int currentSize = sortedNoteQueue.size();
+
+ if (prevSize < 2)
+ return;
+
+ // Look at what should have been the next note,
+ // see if this index exists in new sortedNote vector.
+ // if it does, set the notePos to the index of this.
+ // If not, we keep moving forward to next note after that
+ // and loop around until we find a note that matches
+ int newNotePos = notePos_;
+ int start = (nextNotePos_ + prevSize) % prevSize;
+ int q = start;
+ do
+ {
+ bool noteFound = false;
+ auto prevNote = prevSortedNoteQueue[q].noteNumber;
+
+ for (uint8_t i = 0; i < sortedNoteQueue.size(); i++)
+ {
+ if (sortedNoteQueue[i].noteNumber == prevNote)
+ {
+ newNotePos = i;
+ noteFound = true;
+ q = start;
+ break;
+ ;
+ }
+ }
+
+ if (!noteFound)
+ {
+ q = goingUp_ ? (q + 1) : (q - 1);
+ if (q < 0 || q >= prevSize)
+ {
+ q = start;
+ }
+ // q = (q + prevSize) % prevSize;
+ }
+
+ } while (q != start);
+
+ if (newNotePos == prevNotePos_)
+ return;
+
+ notePos_ = newNotePos;
+
+ // if(goingUp_ && newNotePos >= notePos_)
+ // {
+ // notePos_ = newNotePos;
+ // }
+ // else if(!goingUp_ && newNotePos <= notePos_)
+ // {
+ // notePos_ = newNotePos;
+ // }
+ }
+
+ // Used with stoping sequencers
+ void MidiFXArpeggiator::resync()
+ {
+ playedNoteQueue.clear();
+ holdNoteQueue.clear();
+ sortedNoteQueue.clear();
+ tempNoteQueue.clear();
+
+ resetArpSeq();
+
+ noteMaster.clear();
+
+ // for (uint8_t i = 0; i < 8; i++)
+ // {
+ // trackingNoteGroups[i].prevNoteNumber = 255;
+ // }
+ }
+
+ void MidiFXArpeggiator::onClockTick()
+ {
+ // if (pendingStart_ && omxUtil.areClocksRunning())
+ // {
+ // doPendingStart();
+ // }
+
+
+ if(!pendingStart_) return;
+
+ uint8_t quantIndex = quantizedRateIndex_ < 0 ? clockConfig.globalQuantizeStepIndex : quantizedRateIndex_; // Use global or local quantize rate?
+
+ bool isQuantizedStep = seqConfig.currentClockTick % ((96 * 4) / kArpRates[quantIndex]) == 0;
+
+ // Move pending notes to active
+ if(isQuantizedStep)
+ {
+ doPendingStart();
+ }
+ }
+
+ void MidiFXArpeggiator::updateMultiplier()
+ {
+ if (!multiplierCalculated_)
+ {
+ uint8_t rate = kArpRates[rateIndex_]; // 8
+ multiplier_ = 1.0f / (float)rate; // 1 / 8 = 0.125 // Only need to recalculate this if rate changes yo
+ multiplierCalculated_ = true;
+ }
+ }
+
+ void MidiFXArpeggiator::loopUpdate()
+ {
+ if (messageTextTimer > 0)
+ {
+ messageTextTimer -= sysSettings.timeElasped;
+ if (messageTextTimer <= 0)
+ {
+ omxDisp.setDirty();
+ omxLeds.setDirty();
+ messageTextTimer = 0;
+ }
+ }
+
+ auto now = seqConfig.currentFrameMicros;
+
+ // Send arp offs for notes that had fixed lengths
+ auto it = fixedLengthNotes.begin();
+ while (it != fixedLengthNotes.end())
+ {
+ // remove matching note numbers
+ if (it->offTime <= now)
+ {
+ auto nt = it->noteCache.toMidiNoteGroup();
+ // Serial.println("Removing pending note");
+ arpNoteOff(&nt);
+ // `erase()` invalidates the iterator, use returned iterator
+ it = fixedLengthNotes.erase(it);
+ }
+ else
+ {
+ ++it;
+ }
+ }
+
+ // if (patternDirty_)
+ // {
+ // regeneratePattern();
+ // patternDirty_ = false;
+ // }
+
+ // if (pendingStart_ && !omxUtil.areClocksRunning() && micros() - pendingStartTime_ >= 15000)
+ // {
+ // omxUtil.resetClocks();
+ // doPendingStart();
+ // }
+
+ // Serial.println("arpRunning_: " + String(arpRunning_));
+
+ if (!arpRunning_)
+ {
+ return;
+ }
+
+ // if (sysSettings.omxMode == MODE_MIDI && !selected_)
+ // {
+ // return;
+ // }
+
+ // seqPerc_ = (stepmicros - startMicros) / ((float)max(stepMicroDelta_, 1) * (steps_ + 1));
+
+ // if (steps_ == 0)
+ // {
+ // seqPerc_ = 0;
+
+ // return;
+ // }
+
+ // uint32_t nextBarMicros = stepMicroDelta_ * (steps_ + 1);
+
+ updateMultiplier();
+
+ uint32_t stepmicros = seqConfig.currentFrameMicros;
+
+ if (stepmicros >= nextArpTriggerTime_)
+ {
+ // lastStepTimeP_ = nextStepTimeP_;
+
+ // stepMicroDelta_ = (clockConfig.step_micros * 16) * multiplier_;
+
+ // nextStepTimeP_ = seqConfig.currentFrameMicros + stepMicroDelta_; // calc step based on rate
+
+ nextArpTriggerTime_ = nextArpTriggerTime_ + (clockConfig.step_micros * 16 * multiplier_);
+
+ // nextArpTriggerTime_ = nextStepTimeP_;
+
+ arpNoteTrigger();
+
+ // Keeps arp running for a bit on stop so if you play new notes they will be in sync
+ if (pendingStop_)
+ {
+ doPendingStop();
+
+ // pendingStopCount_--;
+ // if (pendingStopCount_ == 0)
+ // {
+ // doPendingStop();
+ // }
+ }
+ }
+ }
+
+ void MidiFXArpeggiator::resetArpSeq()
+ {
+ // Serial.println("resetArpSeq");
+ // patPos_ = 0;
+ transpPos_ = 0;
+ modPos_ = 0;
+ notePos_ = 0;
+ octavePos_ = 0;
+ syncPos_ = 0;
+
+ lastPlayedNoteNumber_ = -127;
+
+ randPrevNote_ = 255;
+
+ goingUp_ = true;
+ resetNextTrigger_ = false;
+
+ prevNotePos_ = 0;
+ nextNotePos_ = 0;
+ }
+
+ void MidiFXArpeggiator::arpNoteTrigger()
+ {
+ if (sortedNoteQueue.size() == 0)
+ {
+ return;
+ }
+
+ uint32_t noteon_micros = seqConfig.currentFrameMicros;
+
+ if (resetNextTrigger_)
+ {
+ resetArpSeq();
+ }
+
+ // if (sortOrderChanged_)
+ // {
+ // findIndexOfNextNotePos();
+ // sortOrderChanged_ = false;
+ // }
+
+ // if (swing_ > 0 && patPos_ % 2 == 0)
+ // {
+ // if (swing_ < 99)
+ // {
+ // noteon_micros = micros() + ((clockConfig.ppqInterval * multiplier_) / (PPQ / 24) * swing_); // full range swing
+ // }
+ // else if (swing_ == 99)
+ // { // random drunken swing
+ // uint8_t rnd_swing = rand() % 95 + 1; // rand 1 - 95 // randomly apply swing value
+ // noteon_micros = micros() + ((clockConfig.ppqInterval * multiplier_) / (PPQ / 24) * rnd_swing);
+ // }
+ // }
+ // else
+ // {
+ // // noteon_micros = micros();
+ // }
+
+ // if(patPos_ >= notePatLength_)
+ // {
+ // // reset pattern
+ // patPos_ = 0;
+ // }
+ bool incrementOctave = false;
+ int currentNotePos = notePos_;
+ int nextNotePos = notePos_;
+ int qLength = sortedNoteQueue.size();
+
+ prevNotePos_ = notePos_;
+
+ // Attempt to keep position in similar place when number of notes change
+ // if(qLength != prevQLength_)
+ // {
+ // notePos_ = map(prevNotePos_, 0, prevQLength_ - 1, 0, qLength - 1);
+ // currentNotePos = notePos_;
+ // }
+ // Eh, not so good.
+
+ // prevNotePos_ = notePos_;
+ prevQLength_ = qLength;
+
+ switch (arpPattern_)
+ {
+ case ARPPAT_UP:
+ case ARPPAT_DOWN:
+ case ARPPAT_CONVERGE:
+ case ARPPAT_DIVERGE:
+ case ARPPAT_HI_UP:
+ case ARPPAT_LOW_UP:
+ case ARPPAT_AS_PLAYED:
+ {
+ if (currentNotePos >= qLength)
+ {
+ currentNotePos = 0;
+ incrementOctave = true;
+ }
+ nextNotePos = currentNotePos + 1;
+ }
+ break;
+ case ARPPAT_UP_DOWN:
+ case ARPPAT_DOWN_UP:
+ case ARPPAT_CONVERGE_DIVERGE:
+ case ARPPAT_HI_UP_DOWN:
+ case ARPPAT_LOW_UP_DOWN:
+ {
+ // Get down
+ if (goingUp_)
+ {
+ // Turn around
+ if (currentNotePos >= qLength)
+ {
+ goingUp_ = false;
+ currentNotePos = qLength - 2;
+ if (sortedNoteQueue.size() <= 4 && (arpPattern_ == ARPPAT_HI_UP_DOWN || arpPattern_ == ARPPAT_LOW_UP_DOWN))
+ {
+ currentNotePos = 0;
+ goingUp_ = true;
+ incrementOctave = true;
+ }
+ // incrementOctave = true;
+ }
+ }
+ // go to town
+ else
+ {
+ int endIndex = 1;
+ // Boot scootin' boogie
+
+ if (arpPattern_ == ARPPAT_HI_UP_DOWN || arpPattern_ == ARPPAT_LOW_UP_DOWN)
+ {
+ // CEGB
+ // BCBEBG-BE-BCBEBG // on updown, down will need to end at index 2
+ // CECGCB-CG
+ // CEG
+ // CECG-CE //
+
+ endIndex = 3;
+ }
+
+ if (currentNotePos < endIndex)
+ {
+ currentNotePos = 0;
+ goingUp_ = true;
+ incrementOctave = true;
+ }
+ }
+
+ if (goingUp_)
+ {
+ nextNotePos = currentNotePos + 1;
+ }
+ else
+ {
+ nextNotePos = currentNotePos - 1;
+ }
+ }
+ break;
+ case ARPPAT_UP_AND_DOWN:
+ case ARPPAT_DOWN_AND_UP:
+ {
+ // Get down
+ if (goingUp_)
+ {
+ // Turn around
+ if (currentNotePos >= qLength)
+ {
+ goingUp_ = false;
+ currentNotePos = qLength - 1;
+ // incrementOctave = true;
+ }
+ }
+ // go to town
+ else
+ {
+ // Boot scootin' boogie
+ if (currentNotePos < 0)
+ {
+ currentNotePos = 0;
+ goingUp_ = true;
+ incrementOctave = true;
+ }
+ }
+
+ if (goingUp_)
+ {
+ nextNotePos = currentNotePos + 1;
+ }
+ else
+ {
+ nextNotePos = currentNotePos - 1;
+ }
+ }
+ break;
+ case ARPPAT_RAND:
+ {
+ currentNotePos = rand() % qLength;
+ if (notePos_ >= qLength)
+ {
+ notePos_ = 0;
+ incrementOctave = true;
+ }
+ nextNotePos = notePos_ + 1;
+ }
+ break;
+ case ARPPAT_RAND_OTHER:
+ {
+ if (qLength == 1)
+ {
+ currentNotePos = 0;
+ }
+ else
+ {
+ // search up to 4 times the queue size for a note that's not the previous
+ for (uint8_t i = 0; i < queueSize * 4; i++)
+ {
+ currentNotePos = rand() % qLength;
+
+ if (sortedNoteQueue[currentNotePos].noteNumber != randPrevNote_)
+ {
+ break;
+ }
+ }
+ }
+ if (notePos_ >= qLength)
+ {
+ notePos_ = 0;
+ incrementOctave = true;
+ }
+ nextNotePos = notePos_ + 1;
+ }
+ break;
+ case ARPPAT_RAND_ONCE:
+ {
+ if (currentNotePos >= qLength)
+ {
+ currentNotePos = 0;
+ incrementOctave = true;
+ sortNotes(); // Resort every time octave increments
+ }
+ nextNotePos = currentNotePos + 1;
+ }
+ break;
+ }
+
+ if (incrementOctave)
+ {
+ octavePos_++;
+ }
+
+ if (octavePos_ > octaveRange_)
+ {
+ // reset octave
+ octavePos_ = 0;
+ if (arpMode_ == ARPMODE_ONESHOT || arpMode_ == ARPMODE_ONCE)
+ {
+ stopArp();
+ return;
+ }
+ }
+
+ syncPos_ = syncPos_ + 1 % 16;
+
+ // if(syncPos_ == 0)
+ // {
+ // stepMicroDelta_ = (clockConfig.step_micros * 16) * multiplier_;
+ // nextStepTimeP_ = seqConfig.lastClockMicros + stepMicroDelta_; // calc step based on rate
+ // }
+
+ currentNotePos = constrain(currentNotePos, 0, qLength - 1);
+
+ ArpNote arpNote = sortedNoteQueue[currentNotePos];
+ randPrevNote_ = arpNote.noteNumber;
+
+ int16_t noteNumber = arpNote.noteNumber;
+
+ noteNumber = applyModPattern(noteNumber, arpNote.channel);
+ stepLength_ = findStepLength(); // Can be changed by ties in mod pattern
+
+ if (noteNumber != -127)
+ {
+ noteNumber = applyTranspPattern(noteNumber);
+
+ // Add octave
+ noteNumber = noteNumber + (octavePos_ * octDistance_);
+ playNote(noteon_micros, noteNumber, velocity_, arpNote.channel);
+ }
+
+ bool seqReset = false;
+
+ // Advance mod pattern
+ modPos_++;
+ if (modPos_ >= modPatternLength_ + 1)
+ {
+ if (resetMode_ == ARPRESET_MODPAT)
+ {
+ resetArpSeq();
+ seqReset = true;
+ }
+ modPos_ = 0;
+ }
+
+ // Advance transpose pattern
+ transpPos_++;
+ if (transpPos_ >= transpPatternLength_ + 1)
+ {
+ if (resetMode_ == ARPRESET_TRANSPOSEPAT)
+ {
+ resetArpSeq();
+ seqReset = true;
+ }
+ transpPos_ = 0;
+ }
+
+ // if (noteNumber != -127)
+ // {
+ // // Add octave
+ // noteNumber = noteNumber + (octavePos_ * 12);
+ // playNote(noteon_micros, noteNumber, velocity_);
+ // }
+
+ if (!seqReset)
+ {
+ notePos_ = nextNotePos;
+
+ nextNotePos_ = (notePos_ + qLength) % qLength;
+ }
+ else
+ {
+ nextNotePos_ = notePos_;
+ }
+
+ prevSortedNoteQueue.clear();
+
+ for (ArpNote a : sortedNoteQueue)
+ {
+ prevSortedNoteQueue.push_back(a);
+ }
+
+ // playNote(noteon_micros, notePat_[patPos_]);
+
+ // patPos_++;
+ }
+
+ int16_t MidiFXArpeggiator::applyModPattern(int16_t noteNumber, uint8_t channel)
+ {
+ uint8_t modMode = modPattern_[modPos_].mod;
+
+ int16_t newNote = noteNumber;
+
+ if (modMode == MODPAT_REPEAT && lastPlayedMod_ == MODPAT_PWRCHORD)
+ {
+ modMode = MODPAT_PWRCHORD;
+ }
+ else if (modMode == MODPAT_REPEAT && lastPlayedMod_ == MODPAT_CHORD)
+ {
+ modMode = MODPAT_CHORD;
+ }
+
+ switch (modMode)
+ {
+ case MODPAT_ARPNOTE:
+ {
+ newNote = noteNumber;
+ }
+ break;
+ case MODPAT_REST:
+ {
+ newNote = -127;
+ }
+ break;
+ case MODPAT_TIE:
+ {
+ newNote = -127;
+ }
+ break;
+ case MODPAT_REPEAT:
+ {
+ newNote = lastPlayedNoteNumber_;
+ }
+ break;
+ case MODPAT_LOWPITCH_OCTAVE:
+ {
+ newNote = lowestPitch_ - 12;
+ newNote = applyTranspPattern(newNote);
+ uint32_t noteon_micros = seqConfig.currentFrameMicros;
+ playNote(noteon_micros, newNote, velocity_, channel);
+ lastPlayedNoteNumber_ = newNote;
+ newNote = -127;
+ }
+ break;
+ case MODPAT_HIGHPITCH_OCTAVE:
+ {
+ newNote = highestPitch_ + 12;
+ newNote = applyTranspPattern(newNote);
+ uint32_t noteon_micros = seqConfig.currentFrameMicros;
+ playNote(noteon_micros, newNote, velocity_, channel);
+ lastPlayedNoteNumber_ = newNote;
+ newNote = -127;
+ }
+ break;
+ case MODPAT_PWRCHORD:
+ {
+ uint32_t noteon_micros = seqConfig.currentFrameMicros;
+ stepLength_ = findStepLength();
+
+ if (sortedNoteQueue.size() > 1)
+ {
+ newNote = lowestPitch_;
+ newNote = applyTranspPattern(newNote);
+ newNote = newNote + (octavePos_ * 12);
+ playNote(noteon_micros, newNote, velocity_, channel);
+
+ newNote = highestPitch_;
+ newNote = applyTranspPattern(newNote);
+ newNote = newNote + (octavePos_ * 12);
+ playNote(noteon_micros, newNote, velocity_, channel);
+
+ newNote = -127; // Don't play this note.
+
+ // lastPlayedNoteNumber_ = -130;
+ lastPlayedMod_ = modMode;
+ lastPlayedNoteNumber_ = newNote;
+ }
+ else // only 1 note in queue
+ {
+ newNote = noteNumber;
+ }
+ }
+ break;
+ case MODPAT_CHORD:
+ {
+ uint32_t noteon_micros = seqConfig.currentFrameMicros;
+ stepLength_ = findStepLength();
+
+ for (ArpNote n : sortedNoteQueue)
+ {
+ newNote = n.noteNumber;
+ newNote = applyTranspPattern(newNote);
+ newNote = newNote + (octavePos_ * 12);
+
+ playNote(noteon_micros, newNote, velocity_, n.channel);
+ }
+
+ lastPlayedMod_ = modMode;
+ lastPlayedNoteNumber_ = newNote;
+
+ // lastPlayedNoteNumber_ = -131;
+
+ newNote = -127; // Don't play this note.
+ }
+ break;
+ case MODPAT_NOTE1:
+ case MODPAT_NOTE2:
+ case MODPAT_NOTE3:
+ case MODPAT_NOTE4:
+ case MODPAT_NOTE5:
+ case MODPAT_NOTE6:
+ {
+ uint8_t noteIndex = modMode - MODPAT_NOTE1;
+
+ if (arpMode_ == ARPMODE_ON || arpMode_ == ARPMODE_ONCE)
+ {
+ if (noteIndex < playedNoteQueue.size())
+ {
+ newNote = playedNoteQueue[noteIndex].noteNumber;
+ }
+ else
+ {
+ newNote = -127;
+ }
+ }
+ else // Hold or one shot
+ {
+ if (noteIndex < holdNoteQueue.size())
+ {
+ newNote = holdNoteQueue[noteIndex].noteNumber;
+ }
+ else
+ {
+ newNote = -127;
+ }
+ }
+ }
+ break;
+ }
+
+ if (newNote != -127)
+ {
+ lastPlayedMod_ = modMode;
+ lastPlayedNoteNumber_ = newNote;
+ }
+
+ return newNote;
+ }
+
+ uint8_t MidiFXArpeggiator::findStepLength()
+ {
+ uint8_t len = 1;
+
+ for (uint8_t i = 1; i < 16; i++)
+ {
+ uint8_t modIndex = (modPos_ + i) % (modPatternLength_ + 1);
+ uint8_t mod = modPattern_[modIndex].mod;
+ if (mod == MODPAT_TIE)
+ {
+ // Increase length for each tie
+ len++;
+ }
+ else
+ {
+ break;
+ }
+ }
+
+ return len;
+ }
+
+ int16_t MidiFXArpeggiator::applyTranspPattern(int16_t noteNumber)
+ {
+ // Simple
+ int16_t newNote = noteNumber + transpPattern_[transpPos_];
+
+ return newNote;
+ }
+
+ void MidiFXArpeggiator::playNote(uint32_t noteOnMicros, int16_t noteNumber, uint8_t velocity, uint8_t channel)
+ {
+ // Serial.println("PlayNote: " + String(note.noteNumber));
+ if (noteNumber < 0 || noteNumber > 127)
+ return;
+
+ MidiNoteGroup noteOut;
+
+ noteOut.channel = channel + 1;
+ noteOut.noteNumber = (uint8_t)noteNumber;
+ noteOut.prevNoteNumber = (uint8_t)noteNumber;
+ noteOut.velocity = velocity;
+ noteOut.stepLength = ((float)gate * 0.01f) * (16.0f * multiplier_) * (float)stepLength_;
+ noteOut.sendMidi = sendMidi_;
+ noteOut.sendCV = sendCV_;
+ noteOut.noteonMicros = noteOnMicros;
+ noteOut.unknownLength = false;
+ noteOut.noteOff = false;
+
+ // lastPlayedNoteNumber_ = noteNumber;
+
+ sendNoteOut(noteOut);
+ }
+
+ void MidiFXArpeggiator::onEncoderChangedEditParam(Encoder::Update enc)
+ {
+ int8_t page = params_.getSelPage();
+ int8_t param = params_.getSelParam();
+
+ // auto amt = enc.accel(5);
+
+ auto amtSlow = enc.accel(1);
+ auto amtFast = enc.accel(5);
+
+ if (page == ARPPAGE_1) // Mode, Pattern, Reset mode, Chance
+ {
+ if (param == 0)
+ {
+ uint8_t prevArpMode = arpMode_;
+ arpMode_ = constrain(arpMode_ + amtSlow, 0, 4);
+ if (prevArpMode != arpMode_ && arpMode_ != ARPMODE_HOLD)
+ {
+ changeArpMode(arpMode_);
+ // if((arpMode_ == ARPMODE_ON && hasMidiNotes() == false) || (arpMode_ == ARPMODE_ONCE && hasMidiNotes() == false) || arpMode_ == ARPMODE_OFF)
+ // {
+ // stopArp();
+ // }
+
+ // switch (arpMode_)
+ // {
+ // case ARPMODE_OFF:
+ // case ARPMODE_ON:
+ // resync();
+ // break;
+ // }
+ // omxDisp.displayMessage(tempString_.c_str());
+ }
+ }
+ else if (param == 1)
+ {
+ uint8_t prevArpPat = arpPattern_;
+ arpPattern_ = constrain(arpPattern_ + amtSlow, 0, ARPPAT_NUM_OF_PATS - 1);
+ if (prevArpPat != arpPattern_)
+ {
+ omxDisp.displayMessage(kPatMsg_[arpPattern_]);
+ sortNotes();
+ }
+ // holdNotes_ = constrain(holdNotes_ + amt, 0, 1);
+ }
+ else if (param == 2)
+ {
+ uint8_t prevResetMode = resetMode_;
+ resetMode_ = constrain(resetMode_ + amtSlow, 0, 4 - 1);
+ if (prevResetMode != resetMode_)
+ {
+ // omxDisp.displayMessage(kResetMsg_[resetMode_]);
+ }
+ }
+ else if (param == 3)
+ {
+ chancePerc_ = constrain(chancePerc_ + amtFast, 0, 100);
+ }
+ }
+ else if (page == ARPPAGE_2) // Rate, Octave Range, Gate, BPM
+ {
+ if (param == 0)
+ {
+ rateIndex_ = constrain(rateIndex_ + amtSlow, 0, kNumArpRates - 1);
+ multiplierCalculated_ = false;
+ }
+ else if (param == 1)
+ {
+ octaveRange_ = constrain(octaveRange_ + amtSlow, 0, 7);
+ }
+ else if (param == 2)
+ {
+ gate = constrain(gate + amtFast, 2, 200);
+ }
+ else if (param == 3)
+ {
+ clockConfig.newtempo = constrain(clockConfig.clockbpm + amtFast, 40, 300);
+ if (clockConfig.newtempo != clockConfig.clockbpm)
+ {
+ // SET TEMPO HERE
+ clockConfig.clockbpm = clockConfig.newtempo;
+ omxUtil.resetClocks();
+ }
+ // rateIndex_ = constrain(rateIndex_ + amt, 0, kNumArpRates - 1);
+ }
+ }
+ else if (page == ARPPAGE_3)
+ {
+ if (param == 0)
+ {
+ octDistance_ = constrain(octDistance_ + amtSlow, -24, 24);
+
+ // velocity_ = constrain(velocity_ + amtFast, 0, 127);
+ }
+ else if (param == 1)
+ {
+ quantizedRateIndex_ = constrain(quantizedRateIndex_ + amtSlow, -2, kNumArpRates - 1);
+ quantizeSync_ = quantizedRateIndex_ >= -1; // -2 for off
+ }
+ // else if (param == 2)
+ // {
+ // sendMidi_ = constrain(sendMidi_ + amtSlow, 0, 1);
+ // }
+ // else if (param == 2)
+ // {
+ // sendCV_ = constrain(sendCV_ + amtSlow, 0, 1);
+ // }
+ }
+ else if (page == ARPPAGE_4) // Velocity, midiChannel_, sendMidi, sendCV
+ {
+ // if (param == 0)
+ // {
+ // midiChannel_ = constrain(midiChannel_ + amtSlow, 0, 15);
+
+ // // velocity_ = constrain(velocity_ + amtFast, 0, 127);
+ // }
+ // else if (param == 1)
+ // {
+ // midiChannel_ = constrain(midiChannel_ + amtSlow, 0, 15);
+ // }
+ // else if (param == 2)
+ // {
+ // sendMidi_ = constrain(sendMidi_ + amtSlow, 0, 1);
+ // }
+ // else if (param == 2)
+ // {
+ // sendCV_ = constrain(sendCV_ + amtSlow, 0, 1);
+ // }
+ }
+ else if (page == ARPPAGE_MODPAT)
+ {
+ if (param < 16)
+ {
+ uint8_t prevMod = modPattern_[param].mod;
+ modPattern_[param].mod = constrain(modPattern_[param].mod + amtSlow, 0, MODPAT_NUM_OF_MODS - 1);
+
+ if (prevMod != modPattern_[param].mod)
+ {
+ headerMessage_ = kArpModMsg_[modPattern_[param].mod];
+ showMessage();
+ }
+ }
+ else
+ {
+ modPatternLength_ = constrain(modPatternLength_ + amtSlow, 0, 15);
+ }
+ }
+ else if (page == ARPPAGE_TRANSPPAT)
+ {
+ if (param < 16)
+ {
+ transpPattern_[param] = constrain(transpPattern_[param] + amtSlow, -48, 48);
+ // transpPattern_[param] = constrain(transpPattern_[param] + amtSlow, 0, 127);
+ }
+ else
+ {
+ transpPatternLength_ = constrain(transpPatternLength_ + amtSlow, 0, 15);
+ }
+ }
+ omxDisp.setDirty();
+ }
+
+ bool MidiFXArpeggiator::usesKeys()
+ {
+ return params_.getSelPage() >= ARPPAGE_MODPAT;
+ }
+ void MidiFXArpeggiator::onKeyUpdate(OMXKeypadEvent e, uint8_t funcKeyMode)
+ {
+ if (e.held())
+ return;
+
+ int thisKey = e.key();
+
+ auto page = params_.getSelPage();
+ auto param = params_.getSelParam();
+
+ if (e.down() && page >= ARPPAGE_MODPAT && heldKey16_ < 0 && thisKey == 3)
+ {
+ funcKeyModLength_ = true;
+ }
+
+ if (!e.down() && thisKey == 3)
+ {
+ funcKeyModLength_ = false;
+ }
+
+ if (funcKeyMode == FUNCKEYMODE_NONE || heldKey16_ >= 0)
+ {
+ if (e.down())
+ {
+ if (page == ARPPAGE_MODPAT || page == ARPPAGE_TRANSPPAT)
+ {
+ if (heldKey16_ >= 0 && thisKey > 0 && thisKey < 11)
+ {
+ if (page == ARPPAGE_MODPAT)
+ {
+ modPattern_[heldKey16_].mod = thisKey - 1;
+ modCopyBuffer_ = thisKey - 1;
+
+ headerMessage_ = kArpModMsg_[modPattern_[param].mod];
+ showMessage();
+ }
+ else if (page == ARPPAGE_TRANSPPAT)
+ {
+ transpPattern_[heldKey16_] = thisKey - 1;
+ transpCopyBuffer_ = thisKey - 1;
+ }
+ }
+ // Select step
+ if (thisKey >= 11)
+ {
+ if (param == 16 || funcKeyModLength_)
+ {
+ if (page == ARPPAGE_MODPAT)
+ {
+ modPatternLength_ = thisKey - 11;
+ }
+ else if (page == ARPPAGE_TRANSPPAT)
+ {
+ transpPatternLength_ = thisKey - 11;
+ }
+
+ heldKey16_ = -1;
+ }
+ else
+ {
+ if (page == ARPPAGE_MODPAT)
+ {
+ modCopyBuffer_ = modPattern_[thisKey - 11].mod;
+ }
+ else if (page == ARPPAGE_TRANSPPAT)
+ {
+ transpCopyBuffer_ = transpPattern_[thisKey - 11];
+ }
+
+ params_.setSelParam(thisKey - 11);
+ heldKey16_ = thisKey - 11;
+ }
+ }
+ }
+ }
+ else
+ {
+ if (thisKey >= 11 && thisKey - 11 == heldKey16_)
+ {
+ heldKey16_ = -1;
+ }
+ }
+ }
+ else if (funcKeyMode == FUNCKEYMODE_F1)
+ {
+ if (page == ARPPAGE_MODPAT || page == ARPPAGE_TRANSPPAT)
+ {
+ if (e.down())
+ {
+ if (thisKey >= 11)
+ {
+ if (page == ARPPAGE_MODPAT)
+ {
+ modPattern_[thisKey - 11].mod = 0;
+ modCopyBuffer_ = 0;
+ }
+ else if (page == ARPPAGE_TRANSPPAT)
+ {
+ transpPattern_[thisKey - 11] = 0;
+ transpCopyBuffer_ = 0;
+ }
+
+ params_.setSelParam(thisKey - 11);
+
+ headerMessage_ = "Reset: " + String(thisKey - 11 + 1);
+ showMessage();
+ }
+ }
+ }
+ }
+ else if (funcKeyMode == FUNCKEYMODE_F2)
+ {
+ if (page == ARPPAGE_MODPAT || page == ARPPAGE_TRANSPPAT)
+ {
+ if (e.down())
+ {
+ if (thisKey >= 11)
+ {
+ if (page == ARPPAGE_MODPAT)
+ {
+ modPattern_[thisKey - 11].mod = modCopyBuffer_;
+ }
+ else if (page == ARPPAGE_TRANSPPAT)
+ {
+ transpPattern_[thisKey - 11] = transpCopyBuffer_;
+ }
+
+ params_.setSelParam(thisKey - 11);
+
+ headerMessage_ = "Pasted: " + String(thisKey - 11 + 1);
+ showMessage();
+ }
+ }
+ }
+ }
+ else if (funcKeyMode == FUNCKEYMODE_F3)
+ {
+ if (page == ARPPAGE_MODPAT || page == ARPPAGE_TRANSPPAT)
+ {
+ if (e.down())
+ {
+ if (thisKey >= 11)
+ {
+ if (page == ARPPAGE_MODPAT)
+ {
+ modCopyBuffer_ = rand() % MODPAT_NUM_OF_MODS;
+ modPattern_[thisKey - 11].mod = modCopyBuffer_;
+ }
+ else if (page == ARPPAGE_TRANSPPAT)
+ {
+ transpCopyBuffer_ = rand() % 12;
+ transpPattern_[thisKey - 11] = transpCopyBuffer_;
+ }
+
+ params_.setSelParam(thisKey - 11);
+
+ headerMessage_ = "Random: " + String(thisKey - 11 + 1);
+ showMessage();
+ }
+ }
+ }
+ }
+ }
+
+ void MidiFXArpeggiator::onKeyHeldUpdate(OMXKeypadEvent e, uint8_t funcKeyMode)
+ {
+ }
+
+ void MidiFXArpeggiator::updateLEDs(uint8_t funcKeyMode)
+ {
+ bool blinkState = omxLeds.getBlinkState();
+
+ auto page = params_.getSelPage();
+ auto param = params_.getSelParam();
+
+ if (heldKey16_ < 0)
+ {
+ auto modLengthColor = (funcKeyModLength_ && blinkState) ? LEDOFF : FUNKONE;
+ strip.setPixelColor(3, modLengthColor);
+
+ // Function Keys
+ if (funcKeyMode == FUNCKEYMODE_F3)
+ {
+ auto f3Color = blinkState ? LEDOFF : FUNKTHREE;
+ strip.setPixelColor(1, f3Color);
+ strip.setPixelColor(2, f3Color);
+ }
+ else
+ {
+ auto f1Color = (funcKeyMode == FUNCKEYMODE_F1 && blinkState) ? LEDOFF : FUNKONE;
+ strip.setPixelColor(1, f1Color);
+
+ auto f2Color = (funcKeyMode == FUNCKEYMODE_F2 && blinkState) ? LEDOFF : FUNKTWO;
+ strip.setPixelColor(2, f2Color);
+ }
+ }
+ else // Key 16 is held, quick change value
+ {
+ const uint32_t vcolor = 0x101010;
+ const uint32_t vcolor2 = 0xD0D0D0;
+
+ if (page == ARPPAGE_MODPAT)
+ {
+ for (uint8_t i = 0; i < 10; i++)
+ {
+ if (modPattern_[heldKey16_].mod == i)
+ {
+ strip.setPixelColor(i + 1, blinkState ? vcolor : LEDOFF);
+ }
+ else
+ {
+ strip.setPixelColor(i + 1, vcolor);
+ }
+ }
+ }
+ else if (page == ARPPAGE_TRANSPPAT)
+ {
+ for (uint8_t i = 0; i < 10; i++)
+ {
+ if (i <= transpPattern_[heldKey16_])
+ {
+ strip.setPixelColor(i + 1, vcolor2);
+ }
+ else
+ {
+ strip.setPixelColor(i + 1, vcolor);
+ }
+ }
+ }
+ }
+
+ if (page == ARPPAGE_MODPAT)
+ {
+ // const auto MSEL = 0xFFC0C0;
+ const uint32_t MASP = ORANGE;
+ // const uint32_t MREST = 0x440600;
+ const uint32_t MREST = 0x100000;
+ const uint32_t MTIE = 0x801000;
+ const uint32_t MREPEAT = RED;
+ const uint32_t MOTHER = 0xFF00FF;
+
+ for (uint8_t i = 0; i < 16; i++)
+ {
+ if (param == i && blinkState) // Selected
+ {
+ // strip.setPixelColor(11 + i, MSEL);
+ }
+ else
+ {
+ if (i < modPatternLength_ + 1)
+ {
+ auto mod = modPattern_[i].mod;
+
+ if (mod == MODPAT_ARPNOTE)
+ {
+ strip.setPixelColor(11 + i, MASP);
+ }
+ else if (mod == MODPAT_REST)
+ {
+ strip.setPixelColor(11 + i, MREST);
+ }
+ else if (mod == MODPAT_TIE)
+ {
+ strip.setPixelColor(11 + i, MTIE);
+ }
+ else if (mod == MODPAT_REPEAT)
+ {
+ strip.setPixelColor(11 + i, MREPEAT);
+ }
+ else
+ {
+ strip.setPixelColor(11 + i, MOTHER);
+ }
+ }
+ }
+ }
+ }
+ else if (page == ARPPAGE_TRANSPPAT)
+ {
+ // const auto TSEL = 0x9090FF;
+ const uint32_t TZERO = 0x0000FF;
+ const uint32_t THIGH = 0x8080FF;
+ const uint32_t TLOW = 0x000020;
+
+ for (uint8_t i = 0; i < 16; i++)
+ {
+ if (param == i && blinkState) // Selected
+ {
+ // strip.setPixelColor(11 + i, TSEL);
+ }
+ else
+ {
+ if (i < transpPatternLength_ + 1)
+ {
+ if (transpPattern_[i] == 0)
+ {
+ strip.setPixelColor(11 + i, TZERO);
+ }
+ else if (transpPattern_[i] > 0)
+ {
+ strip.setPixelColor(11 + i, THIGH);
+ }
+ else
+ {
+ strip.setPixelColor(11 + i, TLOW);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ void MidiFXArpeggiator::showMessage()
+ {
+ const uint8_t secs = 5;
+ messageTextTimer = secs * 100000;
+ omxDisp.setDirty();
+ }
+
+ void MidiFXArpeggiator::onDisplayUpdate(uint8_t funcKeyMode)
+ {
+ int8_t page = params_.getSelPage();
+ bool useLabelHeader = false;
+
+ if (messageTextTimer > 0)
+ {
+ tempStrings[0] = headerMessage_;
+ useLabelHeader = true;
+ }
+
+ if (!useLabelHeader && funcKeyMode == FUNCKEYMODE_NONE && (page == ARPPAGE_MODPAT || page == ARPPAGE_TRANSPPAT))
+ {
+ if (funcKeyModLength_)
+ {
+ useLabelHeader = true;
+
+ tempStrings[0] = "Set Length";
+ }
+ }
+
+ if (!useLabelHeader && funcKeyMode != FUNCKEYMODE_NONE && (page == ARPPAGE_MODPAT || page == ARPPAGE_TRANSPPAT))
+ {
+ useLabelHeader = true;
+
+ if (funcKeyMode == FUNCKEYMODE_F1)
+ {
+ tempStrings[0] = "Reset";
+ // omxDisp.dispGenericModeLabel("Reset", params_.getNumPages(), params_.getSelPage());
+ }
+ else if (funcKeyMode == FUNCKEYMODE_F2)
+ {
+ tempStrings[0] = "Paste";
+ // omxDisp.dispGenericModeLabel("Paste", params_.getNumPages(), params_.getSelPage());
+ }
+ else if (funcKeyMode == FUNCKEYMODE_F3)
+ {
+ tempStrings[0] = "Random";
+ // omxDisp.dispGenericModeLabel("Random", params_.getNumPages(), params_.getSelPage());
+ }
+ }
+
+ if (page == ARPPAGE_MODPAT)
+ {
+ const char *modChars[16];
+ for (uint8_t i = 0; i < 16; i++)
+ {
+ modChars[i] = kArpModDisp_[modPattern_[i].mod];
+ }
+
+ if (useLabelHeader)
+ {
+ const char *labels[1];
+ labels[0] = tempStrings[0].c_str();
+
+ omxDisp.dispChar16(modChars, modPatternLength_ + 1, constrain(params_.getSelParam(), 0, 15), params_.getNumPages(), params_.getSelPage(), getEncoderSelect(), true, labels, 1);
+ }
+ else
+ {
+ const char *labels[3];
+
+ tempStrings[0] = "LEN: " + String(modPatternLength_ + 1);
+
+ if (params_.getSelParam() < 16)
+ {
+ tempStrings[1] = "SEL: " + String(params_.getSelParam() + 1);
+ tempStrings[2] = "MOD: " + String(kArpModDisp_[modPattern_[params_.getSelParam()].mod]);
+ }
+ else
+ {
+ tempStrings[1] = "SEL: -";
+ tempStrings[2] = "MOD: -";
+ }
+
+ labels[0] = tempStrings[0].c_str();
+ labels[1] = tempStrings[1].c_str();
+ labels[2] = tempStrings[2].c_str();
+ omxDisp.dispChar16(modChars, modPatternLength_ + 1, params_.getSelParam(), params_.getNumPages(), params_.getSelPage(), getEncoderSelect(), true, labels, 3);
+ }
+
+ return;
+ }
+ else if (page == ARPPAGE_TRANSPPAT)
+ {
+ if (useLabelHeader)
+ {
+ const char *labels[1];
+ labels[0] = tempStrings[0].c_str();
+
+ omxDisp.dispValues16(transpPattern_, transpPatternLength_ + 1, -10, 10, true, constrain(params_.getSelParam(), 0, 15), params_.getNumPages(), params_.getSelPage(), getEncoderSelect(), true, labels, 1);
+ }
+ else
+ {
+ const char *labels[3];
+
+ tempStrings[0] = "LEN: " + String(transpPatternLength_ + 1);
+
+ if (params_.getSelParam() < 16)
+ {
+ tempStrings[1] = "SEL: " + String(params_.getSelParam() + 1);
+ tempStrings[2] = "OFS: " + String(transpPattern_[params_.getSelParam()]);
+ }
+ else
+ {
+ tempStrings[1] = "SEL: -";
+ tempStrings[2] = "OFS: -";
+ }
+
+ labels[0] = tempStrings[0].c_str();
+ labels[1] = tempStrings[1].c_str();
+ labels[2] = tempStrings[2].c_str();
+
+ omxDisp.dispValues16(transpPattern_, transpPatternLength_ + 1, -10, 10, true, params_.getSelParam(), params_.getNumPages(), params_.getSelPage(), getEncoderSelect(), true, labels, 3);
+ }
+
+ // omxDisp.dispValues16(transpPattern_, transpPatternLength_ + 1, 0, 127, false, params_.getSelParam(), params_.getNumPages(), params_.getSelPage(), encoderSelect_, true, labels, 3);
+ return;
+ }
+ else if(page == ARPPAGE_Chance)
+ {
+ omxDisp.dispParamBar(chancePerc_, chancePerc_, 0, 100, !getEncoderSelect(), false, "Arpeggiator", "Chance");
+ return;
+ }
+
+ omxDisp.clearLegends();
+
+ if (page == ARPPAGE_1) // Mode, Pattern, Reset mode, Chance
+ {
+ omxDisp.setLegend(0, "MODE", kModeDisp_[arpMode_]);
+ omxDisp.setLegend(1, "PAT", kPatDisp_[arpPattern_]);
+ omxDisp.setLegend(2, "RSET", kResetDisp_[resetMode_]);
+ omxDisp.setLegend(3, "CHC%", String(chancePerc_) + "%");
+ }
+ else if (page == ARPPAGE_2) // Rate, Octave Range, Gate, BPM
+ {
+ omxDisp.setLegend(0, "RATE", "1/" + String(kArpRates[rateIndex_]));
+ omxDisp.setLegend(1, "RANG", octaveRange_ + 1);
+ omxDisp.setLegend(2, "GATE", gate);
+ omxDisp.setLegend(3, "BPM", (int)clockConfig.clockbpm);
+ }
+ else if (page == ARPPAGE_3) // Transpose Distance
+ {
+ omxDisp.setLegend(0, "ODIST", octDistance_ >= 0 ? ("+" + String(octDistance_)) : (String(octDistance_)));
+ omxDisp.setLegend(1, "QUANT", quantizedRateIndex_ <= -2, quantizedRateIndex_ == -1 ? "GBL" : "1/" + String(kArpRates[quantizedRateIndex_]));
+ }
+ else if (page == ARPPAGE_4) // Velocity, midiChannel_, sendMidi, sendCV
+ {
+ omxDisp.setLegend(0, "VEL", velocity_);
+ omxDisp.setLegend(1, "CHAN", midiChannel_ + 1);
+ omxDisp.setLegend(2, "MIDI", sendMidi_);
+ omxDisp.setLegend(3, "CV", sendCV_);
+ }
+
+ omxDisp.dispGenericMode2(params_.getNumPages(), params_.getSelPage(), params_.getSelParam(), getEncoderSelect());
+ }
+
+ int MidiFXArpeggiator::saveToDisk(int startingAddress, Storage *storage)
+ {
+ ArpSave arpSave;
+ arpSave.chancePerc = chancePerc_;
+ arpSave.arpMode = arpMode_;
+ arpSave.arpPattern = arpPattern_;
+ arpSave.resetMode = resetMode_;
+ arpSave.midiChannel = midiChannel_;
+ arpSave.swing = swing_;
+ arpSave.rateIndex = rateIndex_;
+ arpSave.quantizedRateIndex_ = quantizedRateIndex_;
+ arpSave.octaveRange = octaveRange_;
+ arpSave.octDistance_ = octDistance_;
+ arpSave.gate = gate;
+ arpSave.modPatternLength = modPatternLength_;
+ arpSave.transpPatternLength = transpPatternLength_;
+
+ for (uint8_t i = 0; i < 16; i++)
+ {
+ arpSave.modPattern[i] = modPattern_[i];
+ arpSave.transpPattern[i] = transpPattern_[i];
+ }
+
+ int saveSize = sizeof(ArpSave);
+
+ auto saveBytesPtr = (byte *)(&arpSave);
+ for (int j = 0; j < saveSize; j++)
+ {
+ storage->write(startingAddress + j, *saveBytesPtr++);
+ }
+
+ return startingAddress + saveSize;
+ }
+
+ int MidiFXArpeggiator::loadFromDisk(int startingAddress, Storage *storage)
+ {
+ int saveSize = sizeof(ArpSave);
+
+ auto arpSave = ArpSave{};
+ auto current = (byte *)&arpSave;
+ for (int j = 0; j < saveSize; j++)
+ {
+ *current = storage->read(startingAddress + j);
+ current++;
+ }
+
+ chancePerc_ = arpSave.chancePerc;
+ arpMode_ = arpSave.arpMode;
+ arpPattern_ = arpSave.arpPattern;
+ resetMode_ = arpSave.resetMode;
+ midiChannel_ = arpSave.midiChannel;
+ swing_ = arpSave.swing;
+ rateIndex_ = arpSave.rateIndex;
+ quantizedRateIndex_ = arpSave.quantizedRateIndex_;
+ octaveRange_ = arpSave.octaveRange;
+ octDistance_ = arpSave.octDistance_;
+ gate = arpSave.gate;
+ modPatternLength_ = arpSave.modPatternLength;
+ transpPatternLength_ = arpSave.transpPatternLength;
+
+ quantizeSync_ = quantizedRateIndex_ >= -1;
+
+ for (uint8_t i = 0; i < 16; i++)
+ {
+ modPattern_[i] = arpSave.modPattern[i];
+ transpPattern_[i] = arpSave.transpPattern[i];
+ }
+
+ changeArpMode(arpMode_);
+ prevArpMode_ = arpMode_;
+
+ return startingAddress + saveSize;
+ }
+}
diff --git a/Archive/OMX-27-firmware/src/midifx/midifx_arpeggiator.h b/Archive/OMX-27-firmware/src/midifx/midifx_arpeggiator.h
new file mode 100644
index 00000000..8f82f11c
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/midifx/midifx_arpeggiator.h
@@ -0,0 +1,360 @@
+#pragma once
+
+#include "midifx_interface.h"
+#include "midifx_notemaster.h"
+
+namespace midifx
+{
+ enum ArpMode
+ {
+ ARPMODE_OFF,
+ ARPMODE_ON,
+ ARPMODE_ONESHOT,
+ ARPMODE_ONCE,
+ ARPMODE_HOLD
+ };
+
+ // in: C,E,G,B
+ enum ArpPattern
+ {
+ ARPPAT_UP, // Plays notes from lowest to highest: CEGB-CEGB
+ ARPPAT_DOWN, // Plays notes from highest to loweest: BGEC-BGEC
+ ARPPAT_UP_DOWN, // Plays notes up then down: CEGBGE-CEGBGE
+ ARPPAT_DOWN_UP, // Plays notes down then up: BGECEG-BGECEG
+ ARPPAT_UP_AND_DOWN, // Plays notes up then down, end notes repeat: CEGBBGEC-CEGBBGEC
+ ARPPAT_DOWN_AND_UP, // Down then up, ends repeat: BGECCEGB
+ ARPPAT_CONVERGE, // Converges notes to center point: CBEG-CBEG
+ ARPPAT_DIVERGE, // Diverges notes from center: GEBC-GEBC
+ ARPPAT_CONVERGE_DIVERGE, // Converges then diverges: CBEGEB-CBEGEB
+ ARPPAT_HI_UP, // Alternates between highest note: BGBEBC-BGBEBC
+ ARPPAT_HI_UP_DOWN, // BGBEBCBE-BGBEBCBE
+ ARPPAT_LOW_UP, // Alternates between lowest note: CECGCB-CECGCB
+ ARPPAT_LOW_UP_DOWN, // CECGCBCG-CECGCBCG
+ ARPPAT_RAND, // Plays notes randomly, same note could get played twice: GGEGCBB-
+ ARPPAT_RAND_OTHER, // Plays notes randomly, but won't play same note in a row: EGEBCEB
+ ARPPAT_RAND_ONCE, // Plays notes randomly only once, so all notes get played: GCBE
+ ARPPAT_AS_PLAYED, // Plays notes in the order they are played
+ ARPPAT_NUM_OF_PATS
+ };
+
+ enum ModPattern
+ {
+ MODPAT_ARPNOTE, // Plays note as generated by arp
+ MODPAT_REST, // Skips note
+ MODPAT_TIE, // Increases length of previous note
+ MODPAT_REPEAT, // Repeats the last note played
+ MODPAT_LOWPITCH_OCTAVE, // Lowest pitch minus 1 octave
+ MODPAT_HIGHPITCH_OCTAVE, // Highest pitch plus 1 octave
+ MODPAT_PWRCHORD, // Plays a power chord of lowest and highest note
+ MODPAT_CHORD, // Plays a chord of all the notes being played
+ MODPAT_NOTE1, // Plays 1st note as played
+ MODPAT_NOTE2, // 2nd note as played
+ MODPAT_NOTE3,
+ MODPAT_NOTE4,
+ MODPAT_NOTE5,
+ MODPAT_NOTE6,
+ MODPAT_NUM_OF_MODS
+ };
+
+ enum ArpResetMode
+ {
+ ARPRESET_NORMAL, // Resets after reaching end of arp pattern and octave range
+ ARPRESET_NOTE, // Resets whenever a new note is added to arp
+ ARPRESET_MODPAT, // Resets after mod pattern is completed
+ ARPRESET_TRANSPOSEPAT // Resets after the transpose pattern is completed
+ };
+
+ class MidiFXArpeggiator : public MidiFXInterface
+ {
+ public:
+ MidiFXArpeggiator();
+ ~MidiFXArpeggiator();
+
+ int getFXType() override;
+ const char *getName() override;
+ const char *getDispName() override;
+
+ MidiFXInterface *getClone() override;
+
+ void onModeChanged() override;
+
+ void loopUpdate() override;
+ void onClockTick() override;
+ void resync() override;
+
+ bool usesKeys() override;
+ void onKeyUpdate(OMXKeypadEvent e, uint8_t funcKeyMode) override;
+ void onKeyHeldUpdate(OMXKeypadEvent e, uint8_t funcKeyMode) override;
+ void updateLEDs(uint8_t funcKeyMode) override;
+
+ void onDisplayUpdate(uint8_t funcKeyMode) override;
+
+ void noteInput(MidiNoteGroup note) override;
+ // MidiFXNoteFunction getInputFunc() override;
+
+ int saveToDisk(int startingAddress, Storage *storage) override;
+ int loadFromDisk(int startingAddress, Storage *storage) override;
+
+ // Toggles between off and previous mode
+ void toggleArp();
+ void toggleHold();
+ void nextArpPattern();
+ void nextOctRange();
+
+ bool isOn();
+ bool isHoldOn();
+
+ uint8_t getOctaveRange();
+
+ protected:
+ void onEnabled() override;
+ void onDisabled() override;
+
+ void onSelected() override;
+ void onDeselected() override;
+
+ void onEncoderChangedEditParam(Encoder::Update enc) override;
+
+ private:
+ struct ArpNote
+ {
+ // bool inUse = false;
+ uint8_t noteNumber;
+ uint8_t channel;
+ // uint8_t velocity : 7;
+ // bool sendMidi = false;
+ // bool sendCV = false;
+
+ ArpNote()
+ {
+ noteNumber = 255;
+ }
+
+ ArpNote(int noteNumber, uint8_t channel)
+ {
+ if (noteNumber < 0 || noteNumber > 127)
+ {
+ noteNumber = 255;
+ }
+ this->noteNumber = noteNumber;
+ this->channel = channel;
+ }
+
+ ArpNote(MidiNoteGroup *noteGroup)
+ {
+ noteNumber = noteGroup->noteNumber;
+ channel = noteGroup->channel - 1;
+ // velocity = noteGroup.velocity;
+ // sendMidi = noteGroup.sendMidi;
+ // sendCV = noteGroup.sendCV;
+ }
+ };
+
+ struct PendingArpNote
+ {
+ MidiNoteGroupCache noteCache;
+ Micros offTime;
+ };
+
+ // In struct to limit bits
+ struct ArpMod
+ {
+ uint8_t mod : 4;
+
+ ArpMod()
+ {
+ mod = 0;
+ }
+ };
+
+ struct ArpSave
+ {
+ uint8_t chancePerc : 7;
+ uint8_t arpMode : 3;
+ uint8_t arpPattern : 5;
+ uint8_t resetMode : 3;
+ uint8_t midiChannel : 4; // 0-15, Add 1 when using
+ uint8_t swing : 7; // max 100
+ uint8_t rateIndex : 4; // max 15
+ int8_t quantizedRateIndex_ : 5; // max 15 or -1 for hz
+ uint8_t octaveRange : 4; // max 7, 0 = 1 octave
+ int8_t octDistance_ : 6; // -24 to 24
+ uint8_t gate; // 0 - 200
+
+ uint8_t modPatternLength : 4; // Max 15
+ ArpMod modPattern[16];
+
+ uint8_t transpPatternLength : 4; // Max 15
+ int8_t transpPattern[16];
+ };
+
+ static inline bool
+ compareArpNote(ArpNote a1, ArpNote a2)
+ {
+ return (a1.noteNumber < a2.noteNumber);
+ }
+
+ uint8_t chancePerc_ : 7;
+
+ uint8_t arpMode_ : 3;
+
+ uint8_t arpPattern_ : 5;
+
+ uint8_t resetMode_ : 3;
+
+ // bool holdNotes_;
+
+ uint8_t midiChannel_ : 4; // 0-15, Add 1 when using
+
+ uint8_t swing_ : 7; // max 100
+
+ uint8_t rateIndex_ : 4; // max 15
+
+ int8_t quantizedRateIndex_ : 5; // max 15 or -1 for hz
+
+ uint8_t octaveRange_ : 4; // max 7, 0 = 1 octave
+ int8_t octDistance_ : 6; // -24 to 24
+
+ uint8_t gate = 90; // 0 - 200
+
+ // int arpSize = sizeof(ArpNote);
+
+ uint8_t velocity_ : 7;
+ bool sendMidi_ = false;
+ bool sendCV_ = false;
+
+ uint8_t randPrevNote_;
+
+ bool quantizeSync_ = true;
+
+ bool pendingStart_ = false;
+ bool pendingStop_ = false;
+ Micros nextArpTriggerTime_;
+
+ // Micros pendingStartTime_;
+ // uint8_t pendingStopCount_ = 0;
+
+ bool arpRunning_ = false;
+
+ bool multiplierCalculated_ = false;
+
+ static const int queueSize = 8;
+
+ std::vector playedNoteQueue; // Keeps track of which notes are being played
+ std::vector holdNoteQueue; // Holds notes
+ std::vector sortedNoteQueue; // Notes that are used in arp
+ std::vector tempNoteQueue; // Notes that are used in arp
+
+ std::vector prevSortedNoteQueue;
+
+ std::vector fixedLengthNotes; // Notes that are used in arp
+
+ uint8_t modPatternLength_ : 4; // Max 15
+ ArpMod modPattern_[16];
+
+ uint8_t transpPatternLength_ : 4; // Max 15
+ int8_t transpPattern_[16];
+
+ uint8_t modPos_ : 5;
+ uint8_t transpPos_ : 5;
+ int8_t notePos_;
+ uint8_t octavePos_ : 4;
+ uint8_t syncPos_ : 5;
+
+ uint8_t lowestPitch_;
+ uint8_t highestPitch_;
+ uint8_t stepLength_ = 1; // length of note in arp steps
+
+ // ArpNote notePat_[256];
+ // int notePatLength_ = 0;
+ int patPos_;
+ bool goingUp_;
+
+ bool funcKeyModLength_; // Shortcut key to edit length of mod and transpose patterns without needing to use the encoder
+
+ int8_t heldKey16_ = -1; // Key that is held
+
+ int8_t modCopyBuffer_;
+ int8_t transpCopyBuffer_;
+
+ int16_t lastPlayedNoteNumber_;
+ int8_t lastPlayedMod_;
+
+ // Micros nextStepTimeP_ = 32;
+ // Micros lastStepTimeP_ = 32;
+ // uint32_t stepMicroDelta_ = 0;
+
+ float multiplier_ = 1;
+
+ // String tempString_;
+ // String tempString2_;
+ // String tempString3_;
+
+ String headerMessage_;
+
+ int messageTextTimer = 0;
+
+ // Used for toggling arp
+ uint8_t prevArpMode_ : 3;
+
+ int8_t prevNotePos_;
+ int8_t nextNotePos_;
+ int8_t prevQLength_;
+
+ bool resetNextTrigger_;
+ bool sortOrderChanged_;
+
+ MidiFXNoteMaster noteMaster;
+
+ static void processNoteForwarder(void *context, MidiNoteGroup *note)
+ {
+ static_cast(context)->processNoteInput(note);
+ }
+
+ static void sendNoteOutForwarder(void *context, MidiNoteGroup *note)
+ {
+ static_cast(context)->sendNoteOut(*note);
+ }
+
+ // MidiNoteGroup trackingNoteGroups[8];
+ // MidiNoteGroup trackingNoteGroupsPassthrough[8];
+
+ bool insertMidiNoteQueue(MidiNoteGroup *note);
+ bool removeMidiNoteQueue(MidiNoteGroup *note);
+
+ void findIndexOfNextNotePos();
+
+ void sortNotes();
+ // void generatePattern();
+
+ bool hasMidiNotes();
+
+ // void trackNoteInput(MidiNoteGroup note);
+ // void trackNoteInputPassthrough(MidiNoteGroup note, bool ignoreNoteOns);
+ void processNoteInput(MidiNoteGroup *note);
+
+ void arpNoteOn(MidiNoteGroup *note);
+ void arpNoteOff(MidiNoteGroup *note);
+
+ void startArp();
+ void doPendingStart();
+ void stopArp();
+ void doPendingStop();
+ void resetArpSeq();
+
+ void updateMultiplier();
+
+ void arpNoteTrigger();
+ int16_t applyModPattern(int16_t note, uint8_t channel);
+ uint8_t findStepLength();
+ int16_t applyTranspPattern(int16_t note);
+
+ void playNote(uint32_t noteOnMicros, int16_t noteNumber, uint8_t velocity, uint8_t channel);
+
+ void showMessage();
+
+ bool isModeHold(uint8_t arpMode);
+
+ void changeArpMode(uint8_t newArpMode);
+ };
+}
diff --git a/Archive/OMX-27-firmware/src/midifx/midifx_chance.cpp b/Archive/OMX-27-firmware/src/midifx/midifx_chance.cpp
new file mode 100644
index 00000000..96e62517
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/midifx/midifx_chance.cpp
@@ -0,0 +1,131 @@
+#include "midifx_chance.h"
+#include "../hardware/omx_disp.h"
+
+namespace midifx
+{
+ enum ChancePage
+ {
+ CHPAGE_1
+ };
+
+ MidiFXChance::MidiFXChance()
+ {
+ params_.addPage(1);
+ encoderSelect_ = true;
+ chancePerc_ = random(100);
+ }
+
+ int MidiFXChance::getFXType()
+ {
+ return MIDIFX_CHANCE;
+ }
+
+ const char *MidiFXChance::getName()
+ {
+ return "Chance";
+ }
+
+ const char *MidiFXChance::getDispName()
+ {
+ return "CHC";
+ }
+
+ MidiFXInterface *MidiFXChance::getClone()
+ {
+ auto clone = new MidiFXChance();
+ clone->chancePerc_ = chancePerc_;
+ return clone;
+ }
+
+ void MidiFXChance::onEnabled()
+ {
+ }
+
+ void MidiFXChance::onDisabled()
+ {
+ }
+
+ void MidiFXChance::noteInput(MidiNoteGroup note)
+ {
+ if (note.noteOff)
+ {
+ processNoteOff(note);
+ return;
+ }
+
+ // Serial.println("MidiFXChance::noteInput");
+ // note.noteNumber += 7;
+
+ uint8_t r = random(100);
+
+ if (r <= chancePerc_)
+ {
+ processNoteOn(note.noteNumber, note);
+ sendNoteOut(note);
+ }
+ }
+
+ // MidiFXNoteFunction MidiFXChance::getInputFunc()
+ // {
+ // return &MidiFXChance::noteInput;
+ // }
+
+ void MidiFXChance::loopUpdate()
+ {
+ }
+
+ void MidiFXChance::onEncoderChangedEditParam(Encoder::Update enc)
+ {
+ int8_t page = params_.getSelPage();
+ int8_t param = params_.getSelParam();
+
+ auto amt = enc.accel(5);
+
+ if (page == CHPAGE_1)
+ {
+ if (param == 0)
+ {
+ chancePerc_ = constrain(chancePerc_ + amt, 0, 100);
+ }
+ }
+ omxDisp.setDirty();
+ }
+
+ void MidiFXChance::onDisplayUpdate(uint8_t funcKeyMode)
+ {
+ omxDisp.clearLegends();
+
+ int8_t page = params_.getSelPage();
+
+ switch (page)
+ {
+ case CHPAGE_1:
+ {
+ omxDisp.dispParamBar(chancePerc_, chancePerc_, 0, 100, !getEncoderSelect(), false, "Trigger", "Chance");
+ }
+ break;
+ default:
+ break;
+ }
+
+ // omxDisp.dispGenericMode2(params_.getNumPages(), params_.getSelPage(), params_.getSelParam(), getEncoderSelect());
+ }
+
+ int MidiFXChance::saveToDisk(int startingAddress, Storage *storage)
+ {
+ // Serial.println((String)"Saving mfx chance: " + startingAddress); // 5969
+ // Serial.println((String)"chancePerc_: " + chancePerc_);
+ storage->write(startingAddress, chancePerc_);
+ return startingAddress + 1;
+ }
+
+ int MidiFXChance::loadFromDisk(int startingAddress, Storage *storage)
+ {
+ // Serial.println((String)"Loading mfx chance: " + startingAddress); // 5969
+
+ chancePerc_ = constrain(storage->read(startingAddress), 0, 100);
+ // Serial.println((String)"chancePerc_: " + chancePerc_);
+
+ return startingAddress + 1;
+ }
+}
diff --git a/Archive/OMX-27-firmware/src/midifx/midifx_chance.h b/Archive/OMX-27-firmware/src/midifx/midifx_chance.h
new file mode 100644
index 00000000..10125047
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/midifx/midifx_chance.h
@@ -0,0 +1,39 @@
+#pragma once
+
+#include "midifx_interface.h"
+
+namespace midifx
+{
+
+ class MidiFXChance : public MidiFXInterface
+ {
+ public:
+ MidiFXChance();
+ ~MidiFXChance() {}
+
+ int getFXType() override;
+ const char *getName() override;
+ const char *getDispName() override;
+
+ MidiFXInterface *getClone() override;
+
+ void loopUpdate() override;
+
+ void onDisplayUpdate(uint8_t funcKeyMode) override;
+
+ void noteInput(MidiNoteGroup note) override;
+ // MidiFXNoteFunction getInputFunc() override;
+
+ int saveToDisk(int startingAddress, Storage *storage) override;
+ int loadFromDisk(int startingAddress, Storage *storage) override;
+
+ protected:
+ void onEnabled() override;
+ void onDisabled() override;
+
+ void onEncoderChangedEditParam(Encoder::Update enc) override;
+
+ private:
+ uint8_t chancePerc_ = 100;
+ };
+}
diff --git a/Archive/OMX-27-firmware/src/midifx/midifx_chord.cpp b/Archive/OMX-27-firmware/src/midifx/midifx_chord.cpp
new file mode 100644
index 00000000..f74c0dcd
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/midifx/midifx_chord.cpp
@@ -0,0 +1,641 @@
+#include "midifx_chord.h"
+#include "../hardware/omx_disp.h"
+#include "../utils/omx_util.h"
+#include "../utils/chord_util.h"
+#include "../utils/music_scales.h"
+
+namespace midifx
+{
+ const char* chordTypeLabel = "Chord Type";
+ const char* chordTypeOptions[2] = {"Basic", "Interval"};
+
+ enum MfxChordsModePage
+ {
+ MFXCHRDPAGE_NOTES = 0,
+ MFXCHRDPAGE_CHORDTYPE = 1, // Select Chord Type, Basic or Interval
+ MFXCHRDPAGE_CHANCE = 2, // Select Chord Type, Basic or Interval
+ MFXCHRDPAGE_BASIC_NOTES = 3,
+ MFXCHRDPAGE_CUSTOM_NOTES = 4,
+ MFXCHRDPAGE_SCALES = 3,
+ MFXCHRDPAGE_INT_NOTES = 4,
+ MFXCHRDPAGE_INT_SPREAD = 5,
+ MFXCHRDPAGE_INT_QUART = 6,
+ // MFXCHRDPAGE_1, // Note, Octave, Chord, | numNotes, degree, octave, transpose
+ // MFXCHRDPAGE_3, // | spread, rotate, voicing
+ // MFXCHRDPAGE_4, // | spreadUpDown, quartalVoicing
+ };
+
+ enum MFXChordPage
+ {
+ MFXCHORDPAGE_1
+ };
+
+ MidiFXChord::MidiFXChord()
+ {
+ basicParams_.addPage(1); // MFXCHRDPAGE_NOTES
+ basicParams_.addPage(1); // MFXCHRDPAGE_CHORDTYPE
+ basicParams_.addPage(1); // MFXCHRDPAGE_CHANCE
+ basicParams_.addPage(4); // MFXCHRDPAGE_BASIC_NOTES
+ basicParams_.addPage(6); // MFXCHRDPAGE_CUSTOM_NOTES - Custom chord notes, toggled on and off
+
+ intervalParams_.addPage(1); // MFXCHRDPAGE_NOTES
+ intervalParams_.addPage(1); // MFXCHRDPAGE_CHORDTYPE
+ intervalParams_.addPage(1); // MFXCHRDPAGE_CHANCE
+ intervalParams_.addPage(4); // MFXCHRDPAGE_SCALES
+ intervalParams_.addPage(4); // MFXCHRDPAGE_INT_NOTES
+ intervalParams_.addPage(4); // MFXCHRDPAGE_INT_SPREAD
+ intervalParams_.addPage(4); // MFXCHRDPAGE_INT_QUART
+
+ encoderSelect_ = true;
+ }
+
+ int MidiFXChord::getFXType()
+ {
+ return MIDIFX_CHORD;
+ }
+
+ const char *MidiFXChord::getName()
+ {
+ return "Chord";
+ }
+
+ const char *MidiFXChord::getDispName()
+ {
+ return "CHRD";
+ }
+
+ MidiFXInterface *MidiFXChord::getClone()
+ {
+ auto clone = new MidiFXChord();
+
+ clone->chancePerc_ = chancePerc_;
+ clone->useGlobalScale_ = useGlobalScale_;
+ clone->rootNote_ = rootNote_;
+ clone->scaleIndex_ = scaleIndex_;
+ clone->chord_.CopySettingsFrom(&chord_);
+
+ return clone;
+ }
+
+ void MidiFXChord::onEnabled()
+ {
+ }
+
+ void MidiFXChord::onDisabled()
+ {
+ }
+
+ void MidiFXChord::noteInput(MidiNoteGroup note)
+ {
+ if (note.noteOff)
+ {
+ if(note.prevNoteNumber == lastNote_)
+ {
+ chordNotes_.active = false;
+ }
+
+ processNoteOff(note);
+ return;
+ }
+
+ if (chancePerc_ != 100 && (chancePerc_ == 0 || random(100) > chancePerc_))
+ {
+ sendNoteOut(note);
+ return;
+ }
+
+ lastNote_ = note.prevNoteNumber;
+
+ onChordOn(note);
+
+ // if (playOrigin_)
+ // {
+ // sendNoteOut(note);
+ // }
+
+ // int8_t origNote = note.noteNumber;
+
+ // int8_t sentNoteNumbers[7] = {0, 0, 0, 0, 0, 0, 0};
+
+ // for (uint8_t i = 0; i < 7; i++)
+ // {
+ // if (notes_[i] != 0)
+ // {
+ // int8_t newNoteNumber = constrain(origNote + notes_[i], 0, 127);
+
+ // bool noteAlreadyPlayed = false;
+
+ // for (uint8_t j = 0; j < 7; j++)
+ // {
+ // if (sentNoteNumbers[j] == newNoteNumber)
+ // {
+ // noteAlreadyPlayed = true;
+ // break;
+ // }
+ // }
+
+ // if (!noteAlreadyPlayed)
+ // {
+ // note.noteNumber = constrain(origNote + notes_[i], 0, 127);
+ // sendNoteOut(note);
+ // sentNoteNumbers[i] = newNoteNumber;
+ // }
+ // }
+ // }
+ }
+
+ void MidiFXChord::onChordOn(MidiNoteGroup inNote)
+ {
+ if (useGlobalScale_)
+ {
+ rootNote_ = scaleConfig.scaleRoot;
+ scaleIndex_ = scaleConfig.scalePattern;
+ }
+ // Serial.println("onChordOn: " + String(chordIndex));
+ // if (chordNotes_[chordIndex].active)
+ // {
+ // // Serial.println("chord already active");
+ // return; // This shouldn't happen
+ // }
+
+ int8_t autoOctave = 0;
+
+ if (chord_.type == CTYPE_BASIC)
+ {
+ chord_.note = inNote.noteNumber % 12;
+ autoOctave = (inNote.noteNumber / 12) - 5;
+ }
+ else if (chord_.type == CTYPE_INTERVAL)
+ {
+ // Get the note forced to the current scale
+ // int8_t noteInScale = chordUtil.getMusicScale()->remapNoteToScale(inNote.noteNumber);
+ chord_.degree = MusicScales::getDegreeFromNote(inNote.noteNumber, rootNote_, scaleIndex_);
+ // chord_.basicOct = (inNote.noteNumber / 12) - 5;
+ autoOctave = ((inNote.noteNumber + 12 - rootNote_) / 12) - 6;
+ }
+
+ // if (constructChord(chordIndex))
+ if (chordUtil.constructChord(&chord_, &chordNotes_, autoOctave, rootNote_, scaleIndex_, true))
+ {
+ chordNotes_.active = true;
+ chordNotes_.channel = chord_.mchan + 1;
+
+ // Prevent stuck notes
+ // playedChordNotes_[chordIndex].CopyFrom(chordNotes_[chordIndex]);
+ // uint8_t velocity = chords_[chordIndex].velocity;
+
+ // uint32_t noteOnMicros = micros();
+
+ // Serial.print("Chord: ");
+ for (uint8_t i = 0; i < 6; i++)
+ {
+ int noteNumber = chordNotes_.notes[i];
+
+ if (noteNumber < 0 || noteNumber > 127)
+ {
+ continue;
+ }
+ // uint8_t velocity = chordNotes_.velocities[i];
+
+ // Serial.print("Note: " + String(note));
+ // Serial.print(" Vel: " + String(velocity));
+ // Serial.print("\n");
+
+ // if(note >= 0 && note <= 127)
+ // {
+ // // MM::sendNoteOn(note, velocity, chordNotes_[chordIndex].channel);
+ // pendingNoteOns.insert(note, velocity, chordNotes_[chordIndex].channel, noteOnMicros, false);
+ // }
+
+ inNote.noteNumber = chordNotes_.notes[i];
+ inNote.velocity = chordNotes_.velocities[i];
+
+ sendNoteOut(inNote);
+
+ // doNoteOn(note, chordNotes_[chordIndex].midifx, velocity, chordNotes_[chordIndex].channel);
+ }
+ // Serial.print("\n");
+ }
+ else
+ {
+ // Serial.println("constructChord failed");
+ }
+ }
+
+ // MidiFXNoteFunction MidiFXChord::getInputFunc()
+ // {
+ // return &MidiFXChord::noteInput;
+ // }
+
+ void MidiFXChord::loopUpdate()
+ {
+ }
+
+ void MidiFXChord::calculateRemap()
+ {
+ }
+
+ void MidiFXChord::onEncoderChangedSelectParam(Encoder::Update enc)
+ {
+ auto params = getParams();
+
+ params->changeParam(enc.dir());
+ omxDisp.setDirty();
+ }
+
+ void MidiFXChord::onEncoderChangedEditParam(Encoder::Update enc)
+ {
+ auto params = getParams();
+
+ int8_t selPage = params->getSelPage();
+ int8_t selParam = params->getSelParam() + 1; // Add one for readability
+
+ auto chordPtr = &chord_;
+
+ auto amtSlow = enc.accel(1);
+ auto amtFast = enc.accel(5);
+
+ if (selPage == MFXCHRDPAGE_CHORDTYPE)
+ {
+ chordUtil.onEncoderChangedEditParam(&enc, chordPtr, selParam, 1, CPARAM_CHORD_TYPE);
+ }
+ else if (selPage == MFXCHRDPAGE_CHANCE)
+ {
+ chancePerc_ = constrain(chancePerc_ + amtFast, 0, 100);
+ }
+
+ if (chord_.type == CTYPE_INTERVAL)
+ {
+ if (selPage == MFXCHRDPAGE_SCALES)
+ {
+ if (selParam == 1)
+ {
+ useGlobalScale_ = constrain(useGlobalScale_ + amtSlow, 0, 1);
+ if (amtSlow != 0)
+ {
+ omxDisp.displayMessage((useGlobalScale_ ? "Global: ON" : "Global: OFF"));
+ calculateRemap();
+ }
+ }
+ else if (selParam == 2)
+ {
+ if (useGlobalScale_)
+ {
+ int prevRoot = scaleConfig.scaleRoot;
+ scaleConfig.scaleRoot = constrain(scaleConfig.scaleRoot + amtSlow, 0, 12 - 1);
+ if (prevRoot != scaleConfig.scaleRoot)
+ {
+ calculateRemap();
+ }
+ rootNote_ = scaleConfig.scaleRoot;
+ }
+ else
+ {
+ int prevRoot = rootNote_;
+ rootNote_ = constrain(rootNote_ + amtSlow, 0, 12 - 1);
+ if (prevRoot != rootNote_)
+ {
+ calculateRemap();
+ }
+ }
+ }
+ else if (selParam == 3)
+ {
+ if (useGlobalScale_)
+ {
+ int prevPat = scaleConfig.scalePattern;
+ scaleConfig.scalePattern = constrain(scaleConfig.scalePattern + amtSlow, -1, MusicScales::getNumScales() - 1);
+ if (prevPat != scaleConfig.scalePattern)
+ {
+ omxDisp.displayMessage(MusicScales::getScaleName(scaleConfig.scalePattern));
+ calculateRemap();
+ }
+ scaleIndex_ = scaleConfig.scalePattern;
+ }
+ else
+ {
+ int prevPat = scaleIndex_;
+ scaleIndex_ = constrain(scaleIndex_ + amtSlow, -1, MusicScales::getNumScales() - 1);
+ if (prevPat != scaleIndex_)
+ {
+ omxDisp.displayMessage(MusicScales::getScaleName(scaleIndex_));
+ calculateRemap();
+ }
+ }
+ }
+ }
+ else if (selPage == MFXCHRDPAGE_INT_NOTES)
+ {
+ chordUtil.onEncoderChangedEditParam(&enc, chordPtr, selParam, 1, CPARAM_INT_NUMNOTES);
+ chordUtil.onEncoderChangedEditParam(&enc, chordPtr, selParam, 2, CPARAM_INT_DEGREE);
+ chordUtil.onEncoderChangedEditParam(&enc, chordPtr, selParam, 3, CPARAM_INT_OCTAVE);
+ chordUtil.onEncoderChangedEditParam(&enc, chordPtr, selParam, 4, CPARAM_INT_TRANSPOSE);
+ }
+ else if (selPage == MFXCHRDPAGE_INT_SPREAD)
+ {
+ chordUtil.onEncoderChangedEditParam(&enc, chordPtr, selParam, 1, CPARAM_INT_SPREAD);
+ chordUtil.onEncoderChangedEditParam(&enc, chordPtr, selParam, 2, CPARAM_INT_ROTATE);
+ chordUtil.onEncoderChangedEditParam(&enc, chordPtr, selParam, 3, CPARAM_INT_VOICING);
+ }
+ else if (selPage == MFXCHRDPAGE_INT_QUART)
+ {
+ chordUtil.onEncoderChangedEditParam(&enc, chordPtr, selParam, 1, CPARAM_INT_SPRDUPDOWN);
+ chordUtil.onEncoderChangedEditParam(&enc, chordPtr, selParam, 2, CPARAM_INT_QUARTVOICE);
+ }
+ }
+ else if (chord_.type == CTYPE_BASIC)
+ {
+ if (selPage == MFXCHRDPAGE_BASIC_NOTES)
+ {
+ chordUtil.onEncoderChangedEditParam(&enc, chordPtr, selParam, 1, CPARAM_BAS_NOTE);
+ chordUtil.onEncoderChangedEditParam(&enc, chordPtr, selParam, 2, CPARAM_BAS_OCT);
+ chordUtil.onEncoderChangedEditParam(&enc, chordPtr, selParam, 3, CPARAM_BAS_BALANCE);
+ chordUtil.onEncoderChangedEditParam(&enc, chordPtr, selParam, 4, CPARAM_BAS_CHORD);
+ }
+ else if (selPage == MFXCHRDPAGE_CUSTOM_NOTES)
+ {
+ auto amtSlow = enc.accel(1);
+ int8_t sel = params->getSelParam();
+ chord_.customNotes[sel].note = constrain(chord_.customNotes[sel].note + amtSlow, -48, 48);
+
+ if (amtSlow != 0) // To see notes change on keyboard leds
+ {
+ if (useGlobalScale_)
+ {
+ rootNote_ = scaleConfig.scaleRoot;
+ scaleIndex_ = scaleConfig.scalePattern;
+ }
+
+ chordUtil.constructChord(chordPtr, &chordNotes_, midiSettings.octave, rootNote_, scaleIndex_, true);
+ }
+ }
+ }
+
+ omxDisp.setDirty();
+ }
+
+ ParamManager *MidiFXChord::getParams()
+ {
+ if (chord_.type == CTYPE_BASIC)
+ {
+ basicParams_.setPageEnabled(MFXCHRDPAGE_CUSTOM_NOTES, chord_.chord == kCustomChordPattern);
+ intervalParams_.setSelPageAndParam(basicParams_.getSelPage(), basicParams_.getSelParam());
+
+ return &basicParams_;
+ }
+ else
+ {
+ basicParams_.setSelPageAndParam(intervalParams_.getSelPage(), intervalParams_.getSelParam());
+ return &intervalParams_;
+ }
+ }
+
+ void MidiFXChord::onDisplayUpdate(uint8_t funcKeyMode)
+ {
+ omxDisp.clearLegends();
+
+ auto params = getParams();
+
+ // if (chordEditMode_ == false && (mode_ == CHRDMODE_EDIT) && funcKeyMode_ == FUNCKEYMODE_F1) // Edit mode enter edit mode
+ // {
+ // omxDisp.dispGenericModeLabel("Edit chord", params->getNumPages(), params->getSelPage());
+ // }
+ // else if (chordEditMode_ == false && (mode_ == CHRDMODE_EDIT) && funcKeyMode_ == FUNCKEYMODE_F2) // Edit mode copy
+ // {
+ // omxDisp.dispGenericModeLabel("Copy to", params->getNumPages(), params->getSelPage());
+ // }
+ // if (chordEditMode_ == false && (mode_ == CHRDMODE_PLAY || mode_ == CHRDMODE_EDIT || mode_ == CHRDMODE_MANSTRUM) && funcKeyMode_ == FUNCKEYMODE_F2) // Play mode copy
+ // {
+ // omxDisp.dispGenericModeLabel("Copy to", params->getNumPages(), params->getSelPage());
+ // }
+ // else if (chordEditMode_ == false && (mode_ == CHRDMODE_PRESET) && funcKeyMode_ == FUNCKEYMODE_F1) // Preset move load
+ // {
+ // omxDisp.dispGenericModeLabel("Load from", params->getNumPages(), params->getSelPage());
+ // }
+ // else if (chordEditMode_ == false && (mode_ == CHRDMODE_PRESET) && funcKeyMode_ == FUNCKEYMODE_F2) // Preset move save
+ // {
+ // omxDisp.dispGenericModeLabel("Save to", params->getNumPages(), params->getSelPage());
+ // }
+ // else if (chordEditMode_ == false && mode_ == CHRDMODE_MANSTRUM)
+ // {
+ // omxDisp.dispGenericModeLabel("Enc Strum", params->getNumPages(), 0);
+ // }
+ // else
+ if (params->getSelPage() == MFXCHRDPAGE_NOTES)
+ {
+ // if (chordNotes_[selectedChord_].active || chordEditNotes_.active)
+ // {
+ // notesString = "";
+ tempString = "";
+
+ for (uint8_t i = 0; i < 6; i++)
+ {
+ int8_t note = chordNotes_.notes[i];
+
+ if (chordEditNotes_.active)
+ {
+ note = chordEditNotes_.notes[i];
+ }
+
+ if (note >= 0 && note <= 127)
+ {
+ if (i > 0)
+ {
+ tempString.append(" ");
+ }
+ tempString.append(MusicScales::getFullNoteName(note));
+ }
+ }
+
+ const char *labels[1];
+ labels[0] = tempString.c_str();
+ // if (chordEditNotes_.active)
+ // {
+ // // int rootNote = chords_[selectedChord_].note;
+ // omxDisp.dispKeyboard(chordEditNotes_.rootNote, chordEditNotes_.notes, true, labels, 1);
+ // }
+ // else
+ // {
+ // omxDisp.dispKeyboard(chordNotes_[selectedChord_].rootNote, chordNotes_[selectedChord_].notes, true, labels, 1);
+ // }
+
+ omxDisp.dispKeyboard(chordNotes_.rootNote, chordNotes_.notes, true, labels, 1);
+ // }
+ // else
+ // {
+ // omxDisp.dispKeyboard(-1, noNotes, false, nullptr, 0);
+
+ // // omxDisp.dispGenericModeLabel("-", params->getNumPages(), params->getSelPage());
+ // }
+ }
+ else if (params->getSelPage() == MFXCHRDPAGE_CHORDTYPE)
+ {
+ omxDisp.dispOptionCombo(chordTypeLabel, chordTypeOptions, 2, chord_.type, getEncoderSelect());
+ }
+ else if (params->getSelPage() == MFXCHRDPAGE_CHANCE)
+ {
+ omxDisp.dispParamBar(chancePerc_, chancePerc_, 0, 100, !getEncoderSelect(), false, "Chord Trigger", "Chance");
+ }
+ // Chord page
+ else if (params->getSelPage() == MFXCHRDPAGE_BASIC_NOTES && chord_.type == CTYPE_BASIC)
+ {
+ auto noteName = MusicScales::getNoteName(chord_.note, true);
+ int octave = chord_.basicOct + 4;
+ tempString = String(octave);
+ auto chordType = kChordMsg[chord_.chord];
+
+ activeChordBalance_ = chordUtil.getChordBalance(chord_.balance);
+
+ omxDisp.dispChordBasicPage(params->getSelParam(), getEncoderSelect(), noteName, tempString.c_str(), chordType, activeChordBalance_.type, activeChordBalance_.velMult);
+ }
+ // Custom Chord Notes
+ else if (params->getSelPage() == MFXCHRDPAGE_CUSTOM_NOTES && chord_.type == CTYPE_BASIC && chord_.chord == kCustomChordPattern)
+ {
+ const char *labels[6];
+ const char *headers[1];
+ headers[0] = "Custom Chord";
+
+ for (uint8_t i = 0; i < 6; i++)
+ {
+ int note = chord_.customNotes[i].note;
+
+ if (note == 0)
+ {
+ if (i == 0)
+ {
+ tempStrings[i] = "RT";
+ // customNotesStrings[i] = "RT";
+ }
+ else
+ {
+ tempStrings[i] = "-";
+ // customNotesStrings[i] = "-";
+ }
+ }
+ else
+ {
+ if (note > 0)
+ {
+ tempStrings[i] = "+" + String(note);
+ // customNotesStrings[i] = "+" + String(note);
+ }
+ else
+ {
+ tempStrings[i] = "" + String(note);
+ // customNotesStrings[i] = "" + String(note);
+ }
+ }
+
+ labels[i] = tempStrings[i].c_str();
+ // labels[i] = customNotesStrings[i].c_str();
+ }
+
+ omxDisp.dispCenteredSlots(labels, 6, params->getSelParam(), getEncoderSelect(), true, true, headers, 1);
+ }
+ else // Boring generic view
+ {
+ setupPageLegends();
+ omxDisp.dispGenericMode2(params->getNumPages(), params->getSelPage(), params->getSelParam(), getEncoderSelect());
+ }
+ }
+
+ void MidiFXChord::setupPageLegend(ChordSettings *chord, uint8_t index, uint8_t paramType)
+ {
+ }
+
+ void MidiFXChord::setupPageLegends()
+ {
+ omxDisp.clearLegends();
+
+ int8_t page = getParams()->getSelPage();
+
+ auto chordPtr = &chord_;
+
+ if (page == MFXCHRDPAGE_CHORDTYPE)
+ {
+ chordUtil.setupPageLegend(chordPtr, 0, CPARAM_CHORD_TYPE);
+ }
+
+ if (chord_.type == CTYPE_INTERVAL)
+ {
+ switch (page)
+ {
+ case MFXCHRDPAGE_SCALES:
+ {
+ omxDisp.setLegend(0, "GLBL", !useGlobalScale_, "ON");
+ omxDisp.setLegend(1, "ROOT", MusicScales::getNoteName(rootNote_));
+ omxDisp.setLegend(2, "SCALE", scaleIndex_ < 0, scaleIndex_);
+ }
+ break;
+ case MFXCHRDPAGE_INT_NOTES:
+ {
+ chordUtil.setupPageLegend(chordPtr, 0, CPARAM_INT_NUMNOTES);
+ chordUtil.setupPageLegend(chordPtr, 1, CPARAM_INT_DEGREE);
+ chordUtil.setupPageLegend(chordPtr, 2, CPARAM_INT_OCTAVE);
+ chordUtil.setupPageLegend(chordPtr, 3, CPARAM_INT_TRANSPOSE);
+ }
+ break;
+ case MFXCHRDPAGE_INT_SPREAD:
+ {
+ chordUtil.setupPageLegend(chordPtr, 0, CPARAM_INT_SPREAD);
+ chordUtil.setupPageLegend(chordPtr, 1, CPARAM_INT_ROTATE);
+ chordUtil.setupPageLegend(chordPtr, 2, CPARAM_INT_VOICING);
+ }
+ break;
+ case MFXCHRDPAGE_INT_QUART:
+ {
+ chordUtil.setupPageLegend(chordPtr, 0, CPARAM_INT_SPRDUPDOWN);
+ chordUtil.setupPageLegend(chordPtr, 1, CPARAM_INT_QUARTVOICE);
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ int MidiFXChord::saveToDisk(int startingAddress, Storage *storage)
+ {
+ mfxChordSave chordSave;
+ chordSave.chancePerc = chancePerc_;
+ chordSave.useGlobalScale = useGlobalScale_;
+ chordSave.rootNote = rootNote_;
+ chordSave.scaleIndex = scaleIndex_;
+ chordSave.chord.CopySettingsFrom(&chord_);
+
+ int saveSize = sizeof(mfxChordSave);
+
+ auto saveBytesPtr = (byte *)(&chordSave);
+ for (int j = 0; j < saveSize; j++)
+ {
+ storage->write(startingAddress + j, *saveBytesPtr++);
+ }
+
+ startingAddress += saveSize;
+
+ return startingAddress;
+ }
+
+ int MidiFXChord::loadFromDisk(int startingAddress, Storage *storage)
+ {
+ int saveSize = sizeof(mfxChordSave);
+
+ auto chordSave = mfxChordSave{};
+ auto current = (byte *)&chordSave;
+ for (int j = 0; j < saveSize; j++)
+ {
+ *current = storage->read(startingAddress + j);
+ current++;
+ }
+
+ startingAddress += saveSize;
+
+ chancePerc_ = chordSave.chancePerc;
+ useGlobalScale_ = chordSave.useGlobalScale;
+ rootNote_ = chordSave.rootNote;
+ scaleIndex_ = chordSave.scaleIndex;
+ chord_.CopySettingsFrom(&chordSave.chord);
+
+ return startingAddress;
+ }
+}
diff --git a/Archive/OMX-27-firmware/src/midifx/midifx_chord.h b/Archive/OMX-27-firmware/src/midifx/midifx_chord.h
new file mode 100644
index 00000000..28ad6496
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/midifx/midifx_chord.h
@@ -0,0 +1,86 @@
+#pragma once
+
+#include "midifx_interface.h"
+#include "../utils/chord_structs.h"
+
+namespace midifx
+{
+
+ class MidiFXChord : public MidiFXInterface
+ {
+ public:
+ MidiFXChord();
+ ~MidiFXChord() {}
+
+ int getFXType() override;
+ const char *getName() override;
+ const char *getDispName() override;
+
+ MidiFXInterface *getClone() override;
+
+ void loopUpdate() override;
+
+ void onDisplayUpdate(uint8_t funcKeyMode) override;
+
+ void noteInput(MidiNoteGroup note) override;
+
+ int saveToDisk(int startingAddress, Storage *storage) override;
+ int loadFromDisk(int startingAddress, Storage *storage) override;
+
+ protected:
+ void onEnabled() override;
+ void onDisabled() override;
+
+ void onEncoderChangedSelectParam(Encoder::Update enc) override;
+ void onEncoderChangedEditParam(Encoder::Update enc) override;
+
+ private:
+ struct NoteTracker
+ {
+ int8_t triggerCount;
+ uint8_t noteNumber : 7;
+ uint8_t midiChannel : 4;
+ };
+
+ struct mfxChordSave
+ {
+ uint8_t chancePerc : 7;
+ bool useGlobalScale;
+ int8_t rootNote;
+ int8_t scaleIndex;
+
+ ChordSettings chord;
+ };
+
+ uint8_t chancePerc_ = 100;
+
+ uint8_t lastNote_;
+
+ ParamManager basicParams_;
+ ParamManager intervalParams_;
+
+ ChordSettings chord_;
+ ChordNotes chordNotes_;
+ ChordNotes playedChordNotes_;
+ ChordNotes chordEditNotes_;
+
+ ChordBalanceDetails activeChordBalance_;
+
+ int noNotes[6] = {0,0,0,0,0,0};
+
+ const uint8_t kMaxNoteTrackerSize = 32;
+
+ std::vector noteOffTracker;
+
+ bool useGlobalScale_ = true;
+ int8_t rootNote_ = 0;
+ int8_t scaleIndex_ = 0;
+
+ void onChordOn(MidiNoteGroup inNote);
+ ParamManager *getParams();
+
+ void setupPageLegends();
+ void setupPageLegend(ChordSettings *chord, uint8_t index, uint8_t paramType);
+ void calculateRemap();
+ };
+}
diff --git a/Archive/OMX-27-firmware/src/midifx/midifx_harmonizer.cpp b/Archive/OMX-27-firmware/src/midifx/midifx_harmonizer.cpp
new file mode 100644
index 00000000..1c0327cd
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/midifx/midifx_harmonizer.cpp
@@ -0,0 +1,283 @@
+#include "midifx_harmonizer.h"
+#include "../hardware/omx_disp.h"
+
+namespace midifx
+{
+ enum HarmonizerPage
+ {
+ HARMPAGE_1,
+ HARMPAGE_2,
+ HARMPAGE_3
+ };
+
+ MidiFXHarmonizer::MidiFXHarmonizer()
+ {
+ params_.addPage(4);
+ params_.addPage(4);
+ params_.addPage(1);
+
+ encoderSelect_ = true;
+
+ playOrigin_ = true;
+
+ for (uint8_t i = 0; i < 7; i++)
+ {
+ notes_[i] = 0;
+ }
+ }
+
+ int MidiFXHarmonizer::getFXType()
+ {
+ return MIDIFX_HARMONIZER;
+ }
+
+ const char *MidiFXHarmonizer::getName()
+ {
+ return "Harmonizer";
+ }
+
+ const char *MidiFXHarmonizer::getDispName()
+ {
+ return "HARM";
+ }
+
+ MidiFXInterface *MidiFXHarmonizer::getClone()
+ {
+ auto clone = new MidiFXHarmonizer();
+
+ clone->chancePerc_ = chancePerc_;
+ clone->playOrigin_ = playOrigin_;
+
+ for (uint8_t i = 0; i < 7; i++)
+ {
+ clone->notes_[i] = notes_[i];
+ }
+
+ return clone;
+ }
+
+ void MidiFXHarmonizer::onEnabled()
+ {
+ }
+
+ void MidiFXHarmonizer::onDisabled()
+ {
+ }
+
+ void MidiFXHarmonizer::noteInput(MidiNoteGroup note)
+ {
+ if (note.noteOff)
+ {
+ processNoteOff(note);
+ return;
+ }
+
+ if (chancePerc_ != 100 && (chancePerc_ == 0 || random(100) > chancePerc_))
+ {
+ sendNoteOut(note);
+ return;
+ }
+
+ if (playOrigin_)
+ {
+ sendNoteOut(note);
+ }
+
+ int8_t origNote = note.noteNumber;
+
+ int8_t sentNoteNumbers[7] = {0, 0, 0, 0, 0, 0, 0};
+
+ for (uint8_t i = 0; i < 7; i++)
+ {
+ if (notes_[i] != 0)
+ {
+ int8_t newNoteNumber = constrain(origNote + notes_[i], 0, 127);
+
+ bool noteAlreadyPlayed = false;
+
+ for (uint8_t j = 0; j < 7; j++)
+ {
+ if (sentNoteNumbers[j] == newNoteNumber)
+ {
+ noteAlreadyPlayed = true;
+ break;
+ }
+ }
+
+ if (!noteAlreadyPlayed)
+ {
+ note.noteNumber = constrain(origNote + notes_[i], 0, 127);
+ sendNoteOut(note);
+ sentNoteNumbers[i] = newNoteNumber;
+ }
+ }
+ }
+ }
+
+ void MidiFXHarmonizer::loopUpdate()
+ {
+ }
+
+ void MidiFXHarmonizer::onEncoderChangedEditParam(Encoder::Update enc)
+ {
+ int8_t page = params_.getSelPage();
+ int8_t param = params_.getSelParam();
+
+ auto amt = enc.accel(1);
+
+ bool modNote = false;
+ int noteIndex = 0;
+
+ if (page == HARMPAGE_1)
+ {
+ if (param == 0)
+ {
+ playOrigin_ = constrain(playOrigin_ + amt, 0, 1);
+ }
+ else
+ {
+ modNote = true;
+ noteIndex = param - 1;
+ }
+ }
+ else if (page == HARMPAGE_2)
+ {
+ modNote = true;
+ noteIndex = param + 3;
+ }
+ else if (page == HARMPAGE_3)
+ {
+ amt = enc.accel(5);
+ chancePerc_ = constrain(chancePerc_ + amt, 0, 100);
+ }
+
+ if (modNote)
+ {
+ notes_[noteIndex] = constrain(notes_[noteIndex] + amt, -126, 127);
+ }
+
+ omxDisp.setDirty();
+ }
+
+ void MidiFXHarmonizer::onDisplayUpdate(uint8_t funcKeyMode)
+ {
+ omxDisp.clearLegends();
+
+ int8_t page = params_.getSelPage();
+
+ uint8_t starti = 0;
+ // uint8_t endi = 0;
+
+ switch (page)
+ {
+ case HARMPAGE_1:
+ {
+ omxDisp.legends[0] = "ORIG";
+
+ omxDisp.legendVals[0] = -127;
+ omxDisp.legendText[0] = playOrigin_ ? "ON" : "OFF";
+
+ starti = 0;
+ // endi = 3;
+ }
+ break;
+ case HARMPAGE_2:
+ {
+ starti = 3;
+ // endi = 7;
+ }
+ break;
+ case HARMPAGE_3:
+ {
+ omxDisp.legends[0] = "CHC%";
+
+ omxDisp.useLegendString[0] = true;
+ omxDisp.legendString[0] = String(chancePerc_) + "%";
+
+ // // const char* perc[4 + sizeof(char)];
+ // // sprintf(perc, "%d", chancePerc_);
+
+ // tempStringVal_ = String(chancePerc_) + "%";
+
+ // // char perc = static_cast(chancePerc_);
+ // omxDisp.legendVals[0] = -127;
+ // // omxDisp.legendText[0] = &perc + "%";
+ // omxDisp.legendText[0] = tempStringVal_.c_str();
+ }
+ break;
+ default:
+ break;
+ }
+
+ if (page == HARMPAGE_1 || page == HARMPAGE_2)
+ {
+ for (uint8_t i = 0; i < 4; i++)
+ {
+ if (page == HARMPAGE_1 && i == 0)
+ continue;
+
+ // char ch = static_cast(starti + 2);
+
+ tempStrings[i] = "NT " + String(starti + 2);
+ omxDisp.legends[i] = tempStrings[i].c_str();
+ if (notes_[starti] == 0)
+ {
+ // omxDisp.legendVals[i] = -127;
+ // omxDisp.legendText[i] = "--";
+
+ omxDisp.useLegendString[i] = true;
+ omxDisp.legendString[i] = "--";
+ }
+ else if (notes_[starti] > 0)
+ {
+ omxDisp.useLegendString[i] = true;
+ omxDisp.legendString[i] = "+" + String(notes_[starti]);
+
+ // // char nt = static_cast(notes_[starti]);
+ // omxDisp.legendVals[i] = -127;
+
+ // tempStringVal_ = "+" + String(notes_[starti]);
+ // omxDisp.legendText[i] = tempStringVal_.c_str();
+ // // omxDisp.legendText[i] = "+" + nt;
+ }
+ else
+ {
+ omxDisp.legendVals[i] = notes_[starti];
+ }
+
+ starti++;
+ }
+ }
+
+ omxDisp.dispGenericMode2(params_.getNumPages(), params_.getSelPage(), params_.getSelParam(), getEncoderSelect());
+ }
+
+ int MidiFXHarmonizer::saveToDisk(int startingAddress, Storage *storage)
+ {
+ // Serial.println((String) "Saving mfx harmonizer: " + startingAddress); // 5969
+ storage->write(startingAddress + 0, chancePerc_);
+ storage->write(startingAddress + 1, (bool)playOrigin_);
+
+ for (uint8_t i = 0; i < 7; i++)
+ {
+ storage->write(startingAddress + 2 + i, (uint8_t)notes_[i]);
+ }
+
+ return startingAddress + 9;
+ }
+
+ int MidiFXHarmonizer::loadFromDisk(int startingAddress, Storage *storage)
+ {
+ // Serial.println((String) "Loading mfx harmonizer: " + startingAddress); // 5969
+
+ chancePerc_ = storage->read(startingAddress + 0);
+ playOrigin_ = (bool)storage->read(startingAddress + 1);
+
+ for (uint8_t i = 0; i < 7; i++)
+ {
+ notes_[i] = (int8_t)storage->read(startingAddress + 2 + i);
+ }
+
+ return startingAddress + 9;
+ }
+}
diff --git a/Archive/OMX-27-firmware/src/midifx/midifx_harmonizer.h b/Archive/OMX-27-firmware/src/midifx/midifx_harmonizer.h
new file mode 100644
index 00000000..ac9dad09
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/midifx/midifx_harmonizer.h
@@ -0,0 +1,48 @@
+#pragma once
+
+#include "midifx_interface.h"
+
+namespace midifx
+{
+
+ class MidiFXHarmonizer : public MidiFXInterface
+ {
+ public:
+ MidiFXHarmonizer();
+ ~MidiFXHarmonizer() {}
+
+ int getFXType() override;
+ const char *getName() override;
+ const char *getDispName() override;
+
+ MidiFXInterface *getClone() override;
+
+ void loopUpdate() override;
+
+ void onDisplayUpdate(uint8_t funcKeyMode) override;
+
+ void noteInput(MidiNoteGroup note) override;
+
+ int saveToDisk(int startingAddress, Storage *storage) override;
+ int loadFromDisk(int startingAddress, Storage *storage) override;
+
+ protected:
+ void onEnabled() override;
+ void onDisabled() override;
+
+ void onEncoderChangedEditParam(Encoder::Update enc) override;
+
+ private:
+ // std::vector triggeredNotes;
+ bool playOrigin_ = true;
+
+ int8_t notes_[7];
+
+ uint8_t chancePerc_ = 100;
+
+ // String tempString_ = "12345";
+ // String tempStrings_[4] = {"12345", "12345","12345","12345"};
+
+ // String tempStringVal_ = "12345";
+ };
+}
diff --git a/Archive/OMX-27-firmware/src/midifx/midifx_interface.cpp b/Archive/OMX-27-firmware/src/midifx/midifx_interface.cpp
new file mode 100644
index 00000000..d2cba99f
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/midifx/midifx_interface.cpp
@@ -0,0 +1,178 @@
+#include "midifx_interface.h"
+#include "../hardware/omx_disp.h"
+namespace midifx
+{
+ MidiFXInterface::~MidiFXInterface()
+ {
+ // std::vector().swap(triggeredNotes);
+ // Serial.println("Deleted vector");
+ }
+
+ void MidiFXInterface::setSlotIndex(uint8_t slotIndex)
+ {
+ this->mfxSlotIndex_ = slotIndex;
+ }
+
+ void MidiFXInterface::setSelected(bool selected)
+ {
+ bool prevSel = selected_;
+ selected_ = selected;
+
+ if (prevSel != selected_)
+ {
+ if (selected_)
+ {
+ onSelected();
+ }
+ else
+ {
+ onDeselected();
+ }
+ }
+ }
+
+ void MidiFXInterface::setEnabled(bool newEnabled)
+ {
+ enabled_ = newEnabled;
+ if (enabled_)
+ {
+ onEnabled();
+ }
+ else
+ {
+ onDisabled();
+ }
+ }
+
+ bool MidiFXInterface::getEnabled()
+ {
+ return enabled_;
+ }
+
+ bool MidiFXInterface::getEncoderSelect()
+ {
+ return encoderSelect_ && !auxDown_;
+ }
+
+ void MidiFXInterface::onEncoderChanged(Encoder::Update enc)
+ {
+ if (getEncoderSelect())
+ {
+ onEncoderChangedSelectParam(enc);
+ }
+ else
+ {
+ onEncoderChangedEditParam(enc);
+ }
+ }
+
+ void MidiFXInterface::setAuxDown(bool auxDown)
+ {
+ auxDown_ = auxDown;
+ }
+
+ // Handles selecting params using encoder
+ void MidiFXInterface::onEncoderChangedSelectParam(Encoder::Update enc)
+ {
+ params_.changeParam(enc.dir());
+ omxDisp.setDirty();
+ }
+
+ void MidiFXInterface::onEncoderButtonDown()
+ {
+ encoderSelect_ = !encoderSelect_;
+ omxDisp.setDirty();
+ }
+
+ void MidiFXInterface::processNoteOff(MidiNoteGroup note)
+ {
+ // // See if note was previously effected
+ // // Adjust note number if it was and remove from vector
+ // for (size_t i = 0; i < triggeredNotes.size(); i++)
+ // {
+ // if (triggeredNotes[i].prevNoteNumber == note.noteNumber)
+ // {
+ // note.noteNumber = triggeredNotes[i].noteNumber;
+ // triggeredNotes.erase(triggeredNotes.begin() + i);
+ // // Serial.println("Found previous triggered note");
+ // break;
+ // }
+ // }
+
+ // Serial.println("TriggeredNotesSize: " + String(triggeredNotes.size()));
+
+ sendNoteOut(note);
+ }
+
+ void MidiFXInterface::processNoteOn(uint8_t origNoteNumber, MidiNoteGroup note)
+ {
+ // From a keyboard source, length is 0
+ // if(note.stepLength == 0)
+ // {
+ // note.prevNoteNumber = origNoteNumber;
+
+ // bool alreadyExists = false;
+ // // See if orig note alread exists
+ // for (size_t i = 0; i < triggeredNotes.size(); i++)
+ // {
+ // if (triggeredNotes[i].prevNoteNumber == origNoteNumber)
+ // {
+ // triggeredNotes[i] = note;
+ // alreadyExists = true;
+ // // Serial.println("Orig note already existed");
+ // break;
+ // }
+ // }
+
+ // if (!alreadyExists)
+ // {
+ // triggeredNotes.push_back(note);
+ // }
+ // }
+ }
+
+ void MidiFXInterface::setNoteOutput(void (*fptr)(void *, MidiNoteGroup), void *context)
+ {
+ outFunctionContext_ = context;
+ outFunctionPtr_ = fptr;
+ }
+
+ void MidiFXInterface::sendNoteOut(MidiNoteGroup note)
+ {
+ if (outFunctionContext_ != nullptr)
+ {
+ outFunctionPtr_(outFunctionContext_, note);
+ }
+ }
+
+ void MidiFXInterface::sendNoteOff(MidiNoteGroupCache noteCache)
+ {
+ // Serial.println("Note off from cache: " + String(noteCache.noteNumber));
+
+ sendNoteOff(noteCache.toMidiNoteGroup());
+ }
+
+ void MidiFXInterface::sendNoteOff(MidiNoteGroup note)
+ {
+ // Serial.println("Note off: " + String(note.noteNumber));
+
+ note.velocity = 0;
+ note.noteOff = true;
+
+ if (outFunctionContext_ != nullptr)
+ {
+ // Serial.println("Note off sent");
+ outFunctionPtr_(outFunctionContext_, note);
+ }
+ }
+
+ int MidiFXInterface::saveToDisk(int startingAddress, Storage *storage)
+ {
+ return startingAddress;
+ }
+
+ int MidiFXInterface::loadFromDisk(int startingAddress, Storage *storage)
+ {
+ return startingAddress;
+ }
+}
diff --git a/Archive/OMX-27-firmware/src/midifx/midifx_interface.h b/Archive/OMX-27-firmware/src/midifx/midifx_interface.h
new file mode 100644
index 00000000..6bfb022f
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/midifx/midifx_interface.h
@@ -0,0 +1,157 @@
+#pragma once
+#include "../config.h"
+#include "../ClearUI/ClearUI_Input.h"
+#include "../hardware/omx_keypad.h"
+#include "../utils/param_manager.h"
+#include "../hardware/storage.h"
+namespace midifx
+{
+ // Lighter version of MidiNoteGroup for tracking note offs
+ struct MidiNoteGroupCache
+ {
+ uint8_t prevNoteNumber = 0;
+ uint8_t channel = 1;
+ uint8_t noteNumber = 0;
+ bool sendMidi = true;
+ bool sendCV = true;
+ bool unknownLength = false;
+
+ MidiNoteGroupCache()
+ {
+ }
+
+ MidiNoteGroupCache(MidiNoteGroup *noteGroup)
+ {
+ setFromNoteGroup(noteGroup);
+ }
+
+ void setFromNoteGroup(MidiNoteGroup *noteGroup)
+ {
+ prevNoteNumber = noteGroup->prevNoteNumber;
+ channel = noteGroup->channel;
+ noteNumber = noteGroup->noteNumber;
+ sendMidi = noteGroup->sendMidi;
+ sendCV = noteGroup->sendCV;
+ unknownLength = noteGroup->unknownLength;
+ }
+
+ MidiNoteGroup toMidiNoteGroup()
+ {
+ MidiNoteGroup noteGroup;
+ noteGroup.channel = channel;
+ noteGroup.prevNoteNumber = prevNoteNumber;
+ noteGroup.noteNumber = noteNumber;
+ noteGroup.sendCV = sendCV;
+ noteGroup.sendMidi = sendMidi;
+ noteGroup.unknownLength = unknownLength;
+ return noteGroup;
+ }
+ };
+
+ // void(*outNoteptr)(midifxnote); // out pointer type
+
+ // typedef void (*MidiFXNoteFunction)(midifxnote);
+
+ class MidiFXInterface
+ {
+ public:
+ MidiFXInterface() {}
+ virtual ~MidiFXInterface();
+
+ virtual int getFXType() = 0;
+
+ virtual void setSlotIndex(uint8_t slotIndex);
+
+ // Display name
+ virtual const char *getName() = 0;
+
+ // Short Display Name
+ virtual const char *getDispName() = 0;
+
+ virtual uint32_t getColor()
+ {
+ return colorConfig.getMidiFXColor(getFXType());
+ }
+
+ virtual MidiFXInterface *getClone() { return nullptr; }
+
+ // If returns true, midifx will use the keys
+ // Recommend only using keys on specific pages
+ virtual bool usesKeys() { return false; }
+ virtual void onKeyUpdate(OMXKeypadEvent e, uint8_t funcKeyMode) {}
+ virtual void onKeyHeldUpdate(OMXKeypadEvent e, uint8_t funcKeyMode) {}
+ virtual void updateLEDs(uint8_t funcKeyMode) {}
+
+ virtual void onModeChanged(){};
+
+ virtual void setSelected(bool selected);
+
+ virtual void setEnabled(bool newEnabled);
+ virtual bool getEnabled();
+
+ virtual void setAuxDown(bool auxDown);
+
+ virtual void loopUpdate() {}
+ virtual void onClockTick() {}
+
+ virtual void resync() {}
+
+ virtual bool getEncoderSelect();
+
+ virtual void onEncoderChanged(Encoder::Update enc);
+ virtual void onEncoderButtonDown();
+
+ virtual void onDisplayUpdate(uint8_t funcKeyMode) = 0;
+
+ // Static glue to link a pointer to a member function
+ static void onNoteInputForwarder(void *context, MidiNoteGroup note)
+ {
+ static_cast(context)->noteInput(note);
+ }
+
+ virtual void noteInput(MidiNoteGroup note) = 0;
+ // virtual MidiFXNoteFunction getInputFunc() = 0;
+ virtual void setNoteOutput(void (*fptr)(void *, MidiNoteGroup), void *context);
+
+ virtual int saveToDisk(int startingAddress, Storage *storage);
+ virtual int loadFromDisk(int startingAddress, Storage *storage);
+
+ // // the function using the function pointers:
+ // void somefunction(void (*fptr)(void *, int, int), void *context)
+ // {
+ // fptr(context, 17, 42);
+ // }
+
+ protected:
+ bool enabled_ = false;
+ bool selected_ = false;
+ bool auxDown_ = false;
+
+ uint8_t mfxSlotIndex_;
+
+ bool encoderSelect_ = true;
+ ParamManager params_;
+
+ // std::vector triggeredNotes;
+
+ void *outFunctionContext_;
+ void (*outFunctionPtr_)(void *, MidiNoteGroup);
+
+ virtual void onEnabled() {} // Called whenever entering mode
+ virtual void onDisabled() {} // Called whenever entering mode
+
+ virtual void onSelected() {} // Called whenever MidiFX group containing this MidiFX is selected
+ virtual void onDeselected() {} // Called whenever MidiFX group containing this MidiFX is deselected
+
+ virtual void onEncoderChangedSelectParam(Encoder::Update enc);
+ virtual void onEncoderChangedEditParam(Encoder::Update enc) = 0;
+
+ virtual void sendNoteOut(MidiNoteGroup note);
+
+ virtual void sendNoteOff(MidiNoteGroupCache noteCache);
+ virtual void sendNoteOff(MidiNoteGroup note);
+
+ virtual void processNoteOn(uint8_t origNoteNumber, MidiNoteGroup note);
+ virtual void processNoteOff(MidiNoteGroup note);
+ };
+}
diff --git a/Archive/OMX-27-firmware/src/midifx/midifx_monophonic.cpp b/Archive/OMX-27-firmware/src/midifx/midifx_monophonic.cpp
new file mode 100644
index 00000000..f3ea914a
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/midifx/midifx_monophonic.cpp
@@ -0,0 +1,203 @@
+#include "midifx_monophonic.h"
+#include "../hardware/omx_disp.h"
+namespace midifx
+{
+ enum ChancePage
+ {
+ CHPAGE_1
+ };
+
+ MidiFXMonophonic::MidiFXMonophonic()
+ {
+ params_.addPage(4);
+ encoderSelect_ = true;
+ }
+
+ int MidiFXMonophonic::getFXType()
+ {
+ return MIDIFX_MONOPHONIC;
+ }
+
+ const char *MidiFXMonophonic::getName()
+ {
+ return "Make Mono";
+ }
+
+ const char *MidiFXMonophonic::getDispName()
+ {
+ return "MONO";
+ }
+
+ MidiFXInterface *MidiFXMonophonic::getClone()
+ {
+ auto clone = new MidiFXMonophonic();
+
+ clone->chancePerc_ = chancePerc_;
+
+ return clone;
+ }
+
+ void MidiFXMonophonic::onEnabled()
+ {
+ }
+
+ void MidiFXMonophonic::onDisabled()
+ {
+ }
+
+ void MidiFXMonophonic::noteInput(MidiNoteGroup note)
+ {
+ // Serial.println("Mono input: " + String(note.noteNumber) + " " + String(note.channel));
+
+ uint8_t midiChannel = constrain(note.channel - 1, 0, 15);
+
+ if (note.noteOff)
+ {
+ // if (note.unknownLength)
+ // {
+ // if (prevNoteOn[midiChannel].noteNumber == note.noteNumber)
+ // {
+ // // mark empty
+ // prevNoteOn[midiChannel].noteNumber = 255;
+ // }
+ // }
+
+ if (prevNoteOn[midiChannel].noteNumber == note.noteNumber)
+ {
+ // mark empty
+ prevNoteOn[midiChannel].noteNumber = 255;
+ }
+
+ processNoteOff(note);
+ return;
+ }
+
+ // Probability that effect happens
+ if (chancePerc_ != 100 && (chancePerc_ == 0 || random(100) > chancePerc_))
+ {
+ // Serial.println("Skipping mono");
+ sendNoteOut(note);
+ return;
+ }
+
+ // int s = sizeof(MidiNoteGroupCache);
+ // int s2 = sizeof(MidiNoteGroup);
+
+ if (prevNoteOn[midiChannel].noteNumber != 255)
+ {
+ // Serial.println("Prev note found");
+
+ // turn previous note on channel off
+ sendNoteOff(prevNoteOn[midiChannel]);
+ // mark empty
+ // prevNoteOn[midiChannel].noteNumber = 255;
+ }
+ // else
+ // {
+ // Serial.println("Prev note not found");
+ // }
+
+ // Update previous note history
+ prevNoteOn[midiChannel].setFromNoteGroup(¬e);
+
+ sendNoteOut(note);
+
+ // if (note.unknownLength)
+ // {
+ // if (prevNoteOn[midiChannel].noteNumber != 255)
+ // {
+ // // turn previous note on channel off
+ // sendNoteOff(prevNoteOn[midiChannel]);
+ // // mark empty
+ // // prevNoteOn[midiChannel].noteNumber = 255;
+ // }
+
+ // // Update previous note history
+ // prevNoteOn[midiChannel].setFromNoteGroup(note);
+
+ // sendNoteOut(note);
+ // }
+
+ // Serial.println("MidiFXChance::noteInput");
+ // note.noteNumber += 7;
+
+ // uint8_t r = random(255);
+
+ // if(r <= chancePerc_)
+ // {
+ // processNoteOn(note.noteNumber, note);
+ // sendNoteOut(note);
+ // }
+ }
+
+ void MidiFXMonophonic::loopUpdate()
+ {
+ }
+
+ void MidiFXMonophonic::onEncoderChangedEditParam(Encoder::Update enc)
+ {
+ int8_t page = params_.getSelPage();
+ int8_t param = params_.getSelParam();
+
+ auto amt = enc.accel(5);
+
+ if (page == CHPAGE_1)
+ {
+ if (param == 0)
+ {
+ chancePerc_ = constrain(chancePerc_ + amt, 0, 100);
+ }
+ }
+ omxDisp.setDirty();
+ }
+
+ void MidiFXMonophonic::onDisplayUpdate(uint8_t funcKeyMode)
+ {
+ omxDisp.clearLegends();
+
+ int8_t page = params_.getSelPage();
+
+ switch (page)
+ {
+ case CHPAGE_1:
+ {
+ omxDisp.legends[0] = "CHC%";
+ omxDisp.legends[1] = "";
+ omxDisp.legends[2] = "";
+ omxDisp.legends[3] = "";
+ omxDisp.legendVals[0] = -127;
+ omxDisp.legendVals[1] = -127;
+ omxDisp.legendVals[2] = -127;
+ omxDisp.legendVals[3] = -127;
+ omxDisp.useLegendString[0] = true;
+ omxDisp.legendString[0] = String(chancePerc_) + "%";
+
+ // uint8_t perc = ((chancePerc_ / 255.0f) * 100);
+ // String msg = String(perc) + "%";
+ // omxDisp.legendText[0] = msg.c_str();
+ }
+ break;
+ default:
+ break;
+ }
+
+ omxDisp.dispGenericMode2(params_.getNumPages(), params_.getSelPage(), params_.getSelParam(), getEncoderSelect());
+ }
+
+ int MidiFXMonophonic::saveToDisk(int startingAddress, Storage *storage)
+ {
+ // Serial.println((String) "Saving mfx monophonic: " + startingAddress); // 5969
+ storage->write(startingAddress + 0, chancePerc_);
+
+ return startingAddress + 1;
+ }
+
+ int MidiFXMonophonic::loadFromDisk(int startingAddress, Storage *storage)
+ {
+ // Serial.println((String) "Loading mfx monophonic: " + startingAddress); // 5969
+
+ chancePerc_ = storage->read(startingAddress + 0);
+
+ return startingAddress + 1;
+ }
+}
diff --git a/Archive/OMX-27-firmware/src/midifx/midifx_monophonic.h b/Archive/OMX-27-firmware/src/midifx/midifx_monophonic.h
new file mode 100644
index 00000000..937bbdb9
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/midifx/midifx_monophonic.h
@@ -0,0 +1,42 @@
+#pragma once
+
+#include "midifx_interface.h"
+namespace midifx
+{
+
+ // Forces Monophonic output, one note at a time
+ class MidiFXMonophonic : public MidiFXInterface
+ {
+ public:
+ MidiFXMonophonic();
+ ~MidiFXMonophonic() {}
+
+ int getFXType() override;
+ const char *getName() override;
+ const char *getDispName() override;
+
+ MidiFXInterface *getClone() override;
+
+ void loopUpdate() override;
+
+ void onDisplayUpdate(uint8_t funcKeyMode) override;
+
+ void noteInput(MidiNoteGroup note) override;
+ // MidiFXNoteFunction getInputFunc() override;
+
+ int saveToDisk(int startingAddress, Storage *storage) override;
+ int loadFromDisk(int startingAddress, Storage *storage) override;
+
+ protected:
+ void onEnabled() override;
+ void onDisabled() override;
+
+ void onEncoderChangedEditParam(Encoder::Update enc) override;
+
+ private:
+ uint8_t chancePerc_ = 100;
+
+ // 16 midi channels
+ MidiNoteGroupCache prevNoteOn[16];
+ };
+}
diff --git a/Archive/OMX-27-firmware/src/midifx/midifx_notemaster.cpp b/Archive/OMX-27-firmware/src/midifx/midifx_notemaster.cpp
new file mode 100644
index 00000000..b4d647cd
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/midifx/midifx_notemaster.cpp
@@ -0,0 +1,217 @@
+#include "midifx_notemaster.h"
+#include "../hardware/omx_disp.h"
+
+namespace midifx
+{
+ MidiFXNoteMaster::MidiFXNoteMaster()
+ {
+ clear();
+ }
+
+ MidiFXNoteMaster::~MidiFXNoteMaster()
+ {
+ }
+
+ void MidiFXNoteMaster::trackNoteInputPassthrough(MidiNoteGroup *note)
+ {
+ if (note->unknownLength || note->noteOff)
+ {
+ handleNoteInputPassthrough(note);
+ if(note->noteOff)
+ {
+ handleNoteInput(note);
+ }
+ }
+ else
+ {
+ // Notes with known lengths don't need to be tracked.
+ sendNoteOut(note);
+ }
+ }
+
+ void MidiFXNoteMaster::trackNoteInput(MidiNoteGroup *note)
+ {
+ if (note->unknownLength || note->noteOff)
+ {
+ // only notes of unknown lengths need to be tracked
+ // notes with fixed lengths will turn off automatically.
+ if(note->noteOff)
+ {
+ // Whole purpose of this tracking madness is to pass note offs through the chain
+ // that were previously passed through
+ handleNoteInputPassthrough(note);
+ }
+ handleNoteInput(note);
+ }
+ else
+ {
+ processNoteInput(note);
+ }
+ }
+
+ // bool MidiFXNoteMaster::findEmptySlot(MidiNoteGroup *trackingArray, uint8_t size, uint8_t *emptyIndex)
+ // {
+ // for (uint8_t i = 0; i < size; i++)
+ // {
+ // // Found empty slot
+ // if (trackingArray[i].prevNoteNumber == kEmptyIndex)
+ // {
+ // *emptyIndex = i;
+ // return true;
+ // }
+ // }
+ // return false;
+ // }
+
+ void MidiFXNoteMaster::removeFromTracking(MidiNoteGroup *note)
+ {
+ for (uint8_t i = 0; i < kTrackingSize; i++)
+ {
+ if (trackingNoteGroups[i].prevNoteNumber != kEmptyIndex)
+ {
+ if (trackingNoteGroups[i].channel == note->channel && trackingNoteGroups[i].prevNoteNumber == note->prevNoteNumber)
+ {
+ trackingNoteGroups[i].prevNoteNumber = kEmptyIndex; // mark empty
+ }
+ }
+ }
+ }
+
+ void MidiFXNoteMaster::clear()
+ {
+ for (uint8_t i = 0; i < kTrackingSize; i++)
+ {
+ trackingNoteGroups[i].prevNoteNumber = kEmptyIndex;
+ trackingNoteGroupsPassthrough[i].prevNoteNumber = kEmptyIndex;
+ }
+ }
+
+ // If chance is less than 100% and passing through, notes need to be tracked
+ // and if the same note comes in without passthrough for a noteoff event, it needs to
+ // be passed through the effect to send noteoff to prevent stuck notes
+ void MidiFXNoteMaster::handleNoteInputPassthrough(MidiNoteGroup *note)
+ {
+ // Note off
+ if (note->noteOff)
+ {
+ // Search to see if this note is in trackingNoteGroupsPassthrough
+ // If a note is found, it means the note previously had a note on that passed
+ // through and the note off needs to be sent through or else notes could be stuck on.
+ // PrevNoteNumber should be the origin note number before being modified by MidiFX
+ for (uint8_t i = 0; i < kTrackingSize; i++)
+ {
+ if (trackingNoteGroupsPassthrough[i].prevNoteNumber != kEmptyIndex)
+ {
+ if (trackingNoteGroupsPassthrough[i].channel == note->channel && trackingNoteGroupsPassthrough[i].prevNoteNumber == note->prevNoteNumber)
+ {
+ note->noteNumber = trackingNoteGroupsPassthrough[i].noteNumber;
+ sendNoteOut(note);
+ trackingNoteGroupsPassthrough[i].prevNoteNumber = kEmptyIndex; // mark empty
+ }
+ }
+ }
+ }
+ // Note on
+ else
+ {
+ // Search for an empty slot in trackingNoteGroupsPassthrough
+ // If no slots are available/more than 8 notes/ note gets killed.
+ for (uint8_t i = 0; i < kTrackingSize; i++)
+ {
+ // Found empty slot
+ if (trackingNoteGroupsPassthrough[i].prevNoteNumber == kEmptyIndex)
+ {
+ trackingNoteGroupsPassthrough[i].channel = note->channel;
+ trackingNoteGroupsPassthrough[i].prevNoteNumber = note->prevNoteNumber;
+ trackingNoteGroupsPassthrough[i].noteNumber = note->noteNumber;
+
+ // Send it forward through chain
+ sendNoteOut(note);
+ return;
+ }
+ }
+ }
+ }
+
+ void MidiFXNoteMaster::handleNoteInput(MidiNoteGroup *note)
+ {
+ // Same implementation with more comments in submode_midifx
+ // Keeps track of previous note ons and and adjusts note number
+ // for note offs using the prevNoteNumber parameter.
+ // Why is this necessary?
+ // If the note is modified by midifx like randomize before the arp
+ // Then the arp can end up having notes stuck on
+ // This ensures that notes don't get stuck on.
+ if (note->noteOff)
+ {
+ bool noteFound = false;
+
+ for (uint8_t i = 0; i < kTrackingSize; i++)
+ {
+ if (trackingNoteGroups[i].prevNoteNumber != kEmptyIndex)
+ {
+ if (trackingNoteGroups[i].channel == note->channel && trackingNoteGroups[i].prevNoteNumber == note->prevNoteNumber)
+ {
+ // Serial.println("trackNoteInput note off found in trackingNoteGroups");
+ note->noteNumber = trackingNoteGroups[i].noteNumber;
+ processNoteInput(note);
+ trackingNoteGroups[i].prevNoteNumber = kEmptyIndex; // mark empty
+ noteFound = true;
+ }
+ }
+ }
+
+ if (!noteFound)
+ {
+ // Serial.println("trackNoteInput note off not found in trackingNoteGroups");
+
+ processNoteInput(note); // Not sure we need to process note if not found
+ }
+ }
+ else // Note on
+ {
+ for (uint8_t i = 0; i < kTrackingSize; i++)
+ {
+ // Find empty slot
+ if (trackingNoteGroups[i].prevNoteNumber == kEmptyIndex)
+ {
+ trackingNoteGroups[i].channel = note->channel;
+ trackingNoteGroups[i].prevNoteNumber = note->prevNoteNumber;
+ trackingNoteGroups[i].noteNumber = note->noteNumber;
+
+ processNoteInput(note);
+ return;
+ }
+ }
+ }
+ }
+
+ void MidiFXNoteMaster::setContext(void *context)
+ {
+ outFunctionContext_ = context;
+ }
+ void MidiFXNoteMaster::setProcessNoteFptr(void (*fptr)(void *, MidiNoteGroup *))
+ {
+ processNoteFptr = fptr;
+ }
+ void MidiFXNoteMaster::setSendNoteOutFptr(void (*fptr)(void *, MidiNoteGroup *))
+ {
+ sendNoteOutFptr = fptr;
+ }
+
+ void MidiFXNoteMaster::processNoteInput(MidiNoteGroup *note)
+ {
+ if (outFunctionContext_ != nullptr)
+ {
+ processNoteFptr(outFunctionContext_, note);
+ }
+ }
+
+ void MidiFXNoteMaster::sendNoteOut(MidiNoteGroup *note)
+ {
+ if (outFunctionContext_ != nullptr)
+ {
+ sendNoteOutFptr(outFunctionContext_, note);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Archive/OMX-27-firmware/src/midifx/midifx_notemaster.h b/Archive/OMX-27-firmware/src/midifx/midifx_notemaster.h
new file mode 100644
index 00000000..02b98c28
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/midifx/midifx_notemaster.h
@@ -0,0 +1,62 @@
+#pragma once
+#include "../config.h"
+// #include "../ClearUI/ClearUI_Input.h"
+#include "../hardware/omx_keypad.h"
+#include "../utils/param_manager.h"
+
+namespace midifx
+{
+ /// I am the note master
+ /// I am the master of the notes
+ /// all your notes are belong to me
+ /// Not a MidiFX, but a utility for MidiFX to use
+ class MidiFXNoteMaster
+ {
+ public:
+ MidiFXNoteMaster();
+ ~MidiFXNoteMaster();
+
+ void setContext(void *context);
+ void setProcessNoteFptr(void (*fptr)(void *, MidiNoteGroup*));
+ void setSendNoteOutFptr(void (*fptr)(void *, MidiNoteGroup*));
+
+ // Send note here in the case that it is passing through effect
+ // due to chance. IE, passing to next MidiFX slot
+ // If effect is off, you can send note straight through
+ void trackNoteInputPassthrough(MidiNoteGroup *note);
+
+ // Send note here in the case that is is going through the effect
+ void trackNoteInput(MidiNoteGroup *note);
+
+ void removeFromTracking(MidiNoteGroup *note);
+
+ void clear();
+
+
+ private:
+ struct TrackingGroup
+ {
+ uint8_t channel : 5;
+ uint8_t noteNumber;
+ uint8_t prevNoteNumber;
+ };
+
+ void *outFunctionContext_;
+ void (*processNoteFptr)(void *, MidiNoteGroup*);
+ void (*sendNoteOutFptr)(void *, MidiNoteGroup*);
+
+ static const uint8_t kTrackingSize = 8;
+ static const uint8_t kEmptyIndex = 255;
+
+ TrackingGroup trackingNoteGroups[kTrackingSize];
+ TrackingGroup trackingNoteGroupsPassthrough[kTrackingSize];
+
+ void handleNoteInputPassthrough(MidiNoteGroup *note);
+ void handleNoteInput(MidiNoteGroup *note);
+
+ // static bool findEmptySlot(TrackingGroup *trackingArray, uint8_t size, uint8_t *emptyIndex);
+
+ void processNoteInput(MidiNoteGroup *note);
+ void sendNoteOut(MidiNoteGroup *note);
+ };
+}
diff --git a/Archive/OMX-27-firmware/src/midifx/midifx_notetracker.cpp b/Archive/OMX-27-firmware/src/midifx/midifx_notetracker.cpp
new file mode 100644
index 00000000..e69de29b
diff --git a/Archive/OMX-27-firmware/src/midifx/midifx_notetracker.h b/Archive/OMX-27-firmware/src/midifx/midifx_notetracker.h
new file mode 100644
index 00000000..7410d688
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/midifx/midifx_notetracker.h
@@ -0,0 +1,18 @@
+#pragma once
+#include "../config.h"
+// #include "../ClearUI/ClearUI_Input.h"
+#include "../hardware/omx_keypad.h"
+#include "../utils/param_manager.h"
+
+namespace midifx
+{
+ int b = sizeof(MidiNoteGroup);
+ class MidiFXNoteTracker
+ {
+ public:
+ MidiFXNoteTracker() {}
+ virtual ~MidiFXNoteTracker();
+
+ private:
+ };
+}
diff --git a/Archive/OMX-27-firmware/src/midifx/midifx_randomizer.cpp b/Archive/OMX-27-firmware/src/midifx/midifx_randomizer.cpp
new file mode 100644
index 00000000..cb207daa
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/midifx/midifx_randomizer.cpp
@@ -0,0 +1,483 @@
+#include "midifx_randomizer.h"
+#include "../hardware/omx_disp.h"
+#include "../utils/omx_util.h"
+
+namespace midifx
+{
+ enum RandPage {
+ RZPAGE_CHANCE,
+ RZPAGE_1,
+ RZPAGE_2,
+ RZPAGE_3
+ };
+
+ MidiFXRandomizer::MidiFXRandomizer()
+ {
+ params_.addPage(1); // RZPAGE_CHANCE
+ params_.addPage(4); // RZPAGE_1
+ params_.addPage(4); // RZPAGE_2
+ params_.addPage(4); // RZPAGE_3
+ encoderSelect_ = true;
+
+ noteMinus_ = 0;
+ notePlus_ = 0;
+ octMinus_ = 0;
+ octPlus_ = 0;
+ velMinus_ = 0;
+ velPlus_ = 0;
+ lengthPerc_ = 0;
+ chancePerc_ = 100;
+
+ midiChan_ = 0;
+ delayMin_ = 0;
+ delayMax_ = 0;
+ }
+
+ int MidiFXRandomizer::getFXType()
+ {
+ return MIDIFX_RANDOMIZER;
+ }
+
+ const char* MidiFXRandomizer::getName()
+ {
+ return "Randomizer";
+ }
+
+ const char* MidiFXRandomizer::getDispName()
+ {
+ return "RAND";
+ }
+
+ MidiFXInterface* MidiFXRandomizer::getClone()
+ {
+ auto clone = new MidiFXRandomizer();
+
+ clone->noteMinus_ = noteMinus_;
+ clone->notePlus_ = notePlus_;
+ clone->octMinus_ = octMinus_;
+ clone->octPlus_ = octPlus_;
+ clone->velMinus_ = velMinus_;
+ clone->velPlus_ = velPlus_;
+ clone->lengthPerc_ = lengthPerc_;
+ clone->chancePerc_ = chancePerc_;
+ clone->midiChan_ = midiChan_;
+ clone->delayMin_ = delayMin_;
+ clone->delayMax_ = delayMax_;
+
+ return clone;
+ }
+
+ void MidiFXRandomizer::onEnabled()
+ {
+ }
+
+ void MidiFXRandomizer::onDisabled()
+ {
+ }
+
+
+
+
+
+ void MidiFXRandomizer::noteInput(MidiNoteGroup note)
+ {
+ if(note.noteOff)
+ {
+ removeFromDelayQueue(¬e);
+ processNoteOff(note);
+ return;
+ }
+
+ // Serial.println("MidiFXRandomNote::noteInput");
+
+ // Probability that we randomize the note
+ if(chancePerc_ != 100 && (chancePerc_ == 0 || random(100) > chancePerc_))
+ {
+ sendNoteOut(note);
+ return;
+ }
+
+ // int8_t origNote = note.noteNumber;
+
+ int8_t octaveMax = octMinus_ + octPlus_ + 1;
+ int8_t octave = random(0, octaveMax) - octMinus_;
+
+ note.noteNumber = getRand(note.noteNumber, noteMinus_, notePlus_);
+ note.noteNumber = constrain(note.noteNumber + (octave * 12), 0, 127);
+ note.velocity = getRand(note.velocity, velMinus_, velPlus_);
+ note.stepLength = note.stepLength * map(random(lengthPerc_), 0, 100, 1, 16);
+
+ // if(midiChan_ != 0)
+ // {
+ // note.channel = constrain(random(note.channel, note.channel + midiChan_), 1, 16);
+ // }
+
+ if(delayMin_ > 0 || delayMax_ > 0)
+ {
+ processDelayedNote(¬e);
+ }
+ else
+ {
+ processNoteOn(note);
+ }
+ }
+
+ void MidiFXRandomizer::processNoteOff(MidiNoteGroup note)
+ {
+ bool foundNote = false;
+ if (trackedNotes.size() > 0)
+ {
+ auto it = trackedNotes.begin();
+ while (it != trackedNotes.end())
+ {
+ if (it->prevNoteNumber == note.prevNoteNumber && it->origChannel == note.channel)
+ {
+ foundNote = true;
+ // sendNoteOut(note);
+
+ note.channel = it->channel;
+ // note.noteNumber = it->noteNumber;
+
+ sendNoteOut(note);
+
+ it = trackedNotes.erase(it);
+ }
+ else
+ {
+ ++it;
+ }
+ }
+ }
+
+ if(!foundNote)
+ {
+ sendNoteOut(note);
+ }
+ }
+
+ void MidiFXRandomizer::processNoteOn(MidiNoteGroup note)
+ {
+ // Tracking not needed if not randomizing midi chan
+ if(midiChan_ == 0)
+ {
+ sendNoteOut(note);
+ return;
+ }
+
+ if (trackedNotes.size() > 0)
+ {
+ for(auto tNt : trackedNotes)
+ {
+ if (tNt.noteNumber == note.noteNumber && tNt.origChannel == note.channel)
+ {
+ // same note is being tracked already, kill note
+ return;
+ }
+ }
+ }
+
+ if (trackedNotes.size() < queueSize)
+ {
+ RandTrackedNote trackedNote;
+ trackedNote.setFromNoteGroup(¬e);
+
+ // Keep track of original channel
+ trackedNote.origChannel = note.channel;
+
+ note.channel = constrain(random(note.channel, note.channel + midiChan_), 1, 16);
+
+ trackedNote.channel = note.channel;
+
+ trackedNotes.push_back(trackedNote);
+
+ sendNoteOut(note);
+ }
+ // Kill if queue is full
+ }
+
+ void MidiFXRandomizer::removeFromDelayQueue(MidiNoteGroup *note)
+ {
+ if (delayedNoteQueue.size() > 0)
+ {
+ auto it = delayedNoteQueue.begin();
+ while (it != delayedNoteQueue.end())
+ {
+ // Serial.println("activeNoteQueue: " + String(it->noteNumber) + " Chan: " + String(it->channel));
+
+ // TODO track note off midichannels and compare with random one
+ // Might cause problems
+ if (it->noteNumber == note->noteNumber)
+ {
+ // `erase()` invalidates the iterator, use returned iterator
+ it = delayedNoteQueue.erase(it);
+ // foundNoteToRemove = true;
+ }
+ else
+ {
+ ++it;
+ }
+ }
+ }
+
+ // return foundNoteToRemove;
+ }
+
+ void MidiFXRandomizer::processDelayedNote(MidiNoteGroup *note)
+ {
+ if (delayedNoteQueue.capacity() > queueSize)
+ {
+ delayedNoteQueue.shrink_to_fit();
+ }
+
+ if(delayedNoteQueue.size() < queueSize)
+ {
+ float mult = 0.0f;
+ // Assume one of these is greater than 0
+ if (delayMin_ == 0 || delayMax_ == 0)
+ {
+ uint8_t rate = delayMin_ == 0 ? getDelayLength(delayMax_) : getDelayLength(delayMin_);
+ mult = (1.0f / max((float)rate, __FLT_EPSILON__)) * omxUtil.randFloat();
+ }
+ else if (delayMin_ > 0 && delayMax_ > 0)
+ {
+ uint8_t lMin = getDelayLength(delayMin_);
+ uint8_t lMax = getDelayLength(delayMax_);
+
+ float rmin = (float)min(lMin, lMax);
+ float rmax = (float)max(lMin, lMax);
+
+ float rate = omxUtil.lerp(rmin, rmax, omxUtil.randFloat());
+
+ mult = 1.0f / rate;
+ }
+
+ note->noteonMicros = seqConfig.lastClockMicros + (clockConfig.step_micros * 16 * mult);
+
+ delayedNoteQueue.push_back(*note);
+ }
+ else
+ {
+ // queue is filled, send note out
+ processNoteOn(*note);
+ }
+ }
+
+ uint8_t MidiFXRandomizer::getRand(uint8_t v, uint8_t minus, uint8_t plus)
+ {
+ uint8_t minV = max(v - minus, 0);
+ uint8_t maxV = min(v + plus + 1, 127);
+ return random(minV, maxV);
+ }
+
+ void MidiFXRandomizer::loopUpdate()
+ {
+ if(delayedNoteQueue.size() == 0)
+ {
+ return;
+ }
+
+ uint32_t stepmicros = seqConfig.currentFrameMicros;
+
+ auto it = delayedNoteQueue.begin();
+ while (it != delayedNoteQueue.end())
+ {
+ if (stepmicros >= it->noteonMicros)
+ {
+ // Send out and remove
+ processNoteOn(*it);
+ it = delayedNoteQueue.erase(it);
+ }
+ else
+ {
+ ++it;
+ }
+ }
+ }
+
+ void MidiFXRandomizer::onEncoderChangedEditParam(Encoder::Update enc)
+ {
+ int8_t page = params_.getSelPage();
+ int8_t param = params_.getSelParam();
+
+ auto amtSlow = enc.accel(1);
+ auto amtFast = enc.accel(5);
+
+ switch (page)
+ {
+ case RZPAGE_CHANCE:
+ {
+ switch (param)
+ {
+ case 0:
+ chancePerc_ = constrain(chancePerc_ + amtSlow, 0, 100);
+ break;
+ }
+ }
+ break;
+ case RZPAGE_1:
+ {
+ switch (param)
+ {
+ case 0:
+ noteMinus_ = constrain(noteMinus_ + amtSlow, 0, 12);
+ break;
+ case 1:
+ notePlus_ = constrain(notePlus_ + amtSlow, 0, 12);
+ break;
+ case 2:
+ octMinus_ = constrain(octMinus_ + amtSlow, 0, 12);
+ break;
+ case 3:
+ octPlus_ = constrain(octPlus_ + amtSlow, 0, 12);
+ break;
+ }
+ }
+ break;
+ case RZPAGE_2:
+ {
+ switch (param)
+ {
+ case 0:
+ velMinus_ = constrain(velMinus_ + amtFast, 0, 127);
+ break;
+ case 1:
+ velPlus_ = constrain(velPlus_ + amtFast, 0, 127);
+ break;
+ case 2:
+ lengthPerc_ = constrain(lengthPerc_ + amtFast, 0, 100);
+ break;
+ case 3:
+ midiChan_ = constrain(midiChan_ + amtSlow, 0, 16);
+ break;
+ }
+ }
+ break;
+ case RZPAGE_3:
+ {
+ switch (param)
+ {
+ case 0:
+ delayMin_ = constrain(delayMin_ + amtSlow, 0, kNumArpRates); // 0 = off
+ break;
+ case 1:
+ delayMax_ = constrain(delayMax_ + amtSlow, 0, kNumArpRates);
+ break;
+ }
+ }
+ break;
+ }
+
+ omxDisp.setDirty();
+ }
+ uint8_t MidiFXRandomizer::getDelayLength(uint8_t delayIndex)
+ {
+ if(delayIndex == 0 || delayIndex - 1 >= kNumArpRates)
+ {
+ return 0;
+ }
+
+ uint8_t i = delayIndex - 1;
+
+ // Reverse order
+ i = (kNumArpRates - 1) - i;
+
+ return kArpRates[i];
+ }
+
+ void MidiFXRandomizer::onDisplayUpdate(uint8_t funcKeyMode)
+ {
+ omxDisp.clearLegends();
+
+ bool genDisplay = true;
+
+ switch (params_.getSelPage())
+ {
+ case RZPAGE_CHANCE:
+ {
+ omxDisp.dispParamBar(chancePerc_, chancePerc_, 0, 100, !getEncoderSelect(), false, "Rand", "Chance");
+ genDisplay = false;
+ }
+ break;
+ case RZPAGE_1:
+ {
+ omxDisp.setLegend(0, "NT-", noteMinus_);
+ omxDisp.setLegend(1, "NT+", notePlus_);
+ omxDisp.setLegend(2, "OCT-", octMinus_);
+ omxDisp.setLegend(3, "OCT+", octPlus_);
+ }
+ break;
+ case RZPAGE_2:
+ {
+ omxDisp.setLegend(0, "VEL-", velMinus_);
+ omxDisp.setLegend(1, "VEL+", velPlus_);
+ omxDisp.setLegend(2, "LEN%", lengthPerc_);
+ omxDisp.setLegend(3, "CHAN", midiChan_ == 0, midiChan_);
+ }
+ break;
+ case RZPAGE_3:
+ {
+ omxDisp.setLegend(0, "DEL-", delayMin_ == 0, "1/" + String(getDelayLength(delayMin_)));
+ omxDisp.setLegend(1, "DEL+", delayMax_ == 0, "1/" + String(getDelayLength(delayMax_)));
+ }
+ break;
+ }
+
+ if (genDisplay)
+ {
+ omxDisp.dispGenericMode2(params_.getNumPages(), params_.getSelPage(), params_.getSelParam(), getEncoderSelect());
+ }
+ }
+
+ int MidiFXRandomizer::saveToDisk(int startingAddress, Storage *storage)
+ {
+ RandomSave save;
+ save.noteMinus = noteMinus_;
+ save.notePlus = notePlus_;
+ save.octMinus = octMinus_;
+ save.octPlus = octPlus_;
+ save.velMinus = velMinus_;
+ save.velPlus = velPlus_;
+ save.lengthPerc = lengthPerc_;
+ save.chancePerc = chancePerc_;
+ save.midiChan = midiChan_;
+ save.delayMin = delayMin_;
+ save.delayMax = delayMax_;
+
+ int saveSize = sizeof(RandomSave);
+
+ auto saveBytesPtr = (byte *)(&save);
+ for (int j = 0; j < saveSize; j++)
+ {
+ storage->write(startingAddress + j, *saveBytesPtr++);
+ }
+
+ return startingAddress + saveSize;
+ }
+
+ int MidiFXRandomizer::loadFromDisk(int startingAddress, Storage *storage)
+ {
+ int saveSize = sizeof(RandomSave);
+
+ auto save = RandomSave{};
+ auto current = (byte *)&save;
+ for (int j = 0; j < saveSize; j++)
+ {
+ *current = storage->read(startingAddress + j);
+ current++;
+ }
+
+ noteMinus_ = save.noteMinus;
+ notePlus_ = save.notePlus;
+ octMinus_ = save.octMinus;
+ octPlus_ = save.octPlus;
+ velMinus_ = save.velMinus;
+ velPlus_ = save.velPlus;
+ lengthPerc_ = save.lengthPerc;
+ chancePerc_ = save.chancePerc;
+ midiChan_ = save.midiChan;
+ delayMin_ = save.delayMin;
+ delayMax_ = save.delayMax;
+
+ return startingAddress + saveSize;
+ }
+}
diff --git a/Archive/OMX-27-firmware/src/midifx/midifx_randomizer.h b/Archive/OMX-27-firmware/src/midifx/midifx_randomizer.h
new file mode 100644
index 00000000..e81671de
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/midifx/midifx_randomizer.h
@@ -0,0 +1,103 @@
+#pragma once
+
+#include "midifx_interface.h"
+
+namespace midifx
+{
+
+ class MidiFXRandomizer : public MidiFXInterface
+ {
+ public:
+ MidiFXRandomizer();
+ ~MidiFXRandomizer() {}
+
+ int getFXType() override;
+ const char *getName() override;
+ const char *getDispName() override;
+
+ MidiFXInterface *getClone() override;
+
+ void loopUpdate() override;
+
+ void onDisplayUpdate(uint8_t funcKeyMode) override;
+
+ void noteInput(MidiNoteGroup note) override;
+
+ int saveToDisk(int startingAddress, Storage *storage) override;
+ int loadFromDisk(int startingAddress, Storage *storage) override;
+
+
+ protected:
+ void onEnabled() override;
+ void onDisabled() override;
+
+ void onEncoderChangedEditParam(Encoder::Update enc) override;
+
+ void processNoteOff(MidiNoteGroup note) override;
+
+
+ private:
+ struct RandTrackedNote
+ {
+ uint8_t prevNoteNumber = 0;
+ uint8_t channel = 1;
+ uint8_t origChannel = 1;
+ uint8_t noteNumber = 0;
+
+ RandTrackedNote()
+ {
+ }
+
+ void setFromNoteGroup(MidiNoteGroup *noteGroup)
+ {
+ prevNoteNumber = noteGroup->prevNoteNumber;
+ channel = noteGroup->channel;
+ origChannel = noteGroup->channel;
+ noteNumber = noteGroup->noteNumber;
+ }
+ };
+
+ // std::vector triggeredNotes;
+ struct RandomSave
+ {
+ uint8_t noteMinus : 4;
+ uint8_t notePlus : 4;
+ uint8_t octMinus : 4;
+ uint8_t octPlus : 4;
+ uint8_t velMinus : 7;
+ uint8_t velPlus : 7;
+ uint8_t lengthPerc : 7;
+ uint8_t midiChan : 5;
+ uint8_t delayMin : 5;
+ uint8_t delayMax : 5;
+ uint8_t chancePerc : 7;
+ };
+
+ uint8_t noteMinus_ : 4; // 0 to 12
+ uint8_t notePlus_ : 4; // 0 to 12
+ uint8_t octMinus_ : 4; // 0 to 12
+ uint8_t octPlus_ : 4; // 0 to 12
+ uint8_t velMinus_ : 7; // 0 to 127
+ uint8_t velPlus_ : 7; // 0 to 127
+ uint8_t lengthPerc_ : 7; // 0 to 100
+ uint8_t midiChan_ : 5; // 0 to 16, Midi channel random range between incoming channel and channel + this value
+ uint8_t delayMin_ : 5; // Maps to kArpRates, except 0 = off
+ uint8_t delayMax_ : 5;
+ uint8_t chancePerc_ : 7; // 0 to 100
+
+ static const int queueSize = 16;
+ std::vector delayedNoteQueue; // notes pending for quantization
+
+ std::vector trackedNotes; // notes that are tracked because midi chan changed
+
+
+ static uint8_t getDelayLength(uint8_t delayIndex);
+
+ static uint8_t getRand(uint8_t v, uint8_t minus, uint8_t plus);
+
+ void removeFromDelayQueue(MidiNoteGroup *note);
+ void processDelayedNote(MidiNoteGroup *note);
+
+ void processNoteOn(MidiNoteGroup note);
+ };
+}
diff --git a/Archive/OMX-27-firmware/src/midifx/midifx_repeat.cpp b/Archive/OMX-27-firmware/src/midifx/midifx_repeat.cpp
new file mode 100644
index 00000000..d68b3215
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/midifx/midifx_repeat.cpp
@@ -0,0 +1,1386 @@
+#include "midifx_repeat.h"
+#include "../hardware/omx_disp.h"
+#include "../utils/omx_util.h"
+
+namespace midifx
+{
+ Micros nextRepeatTriggerTime_ = 0;
+
+ enum MidiFXRepeatModes
+ {
+ MFXREPEATMODE_OFF,
+ MFXREPEATMODE_ON,
+ MFXREPEATMODE_1SHOT,
+ MFXREPEATMODE_ONCE,
+ MFXREPEATMODE_HOLD,
+ MFXREPEATMODE_COUNT
+ };
+
+ const char *kRepeatModeDisp_[] = {"OFF", "ON", "1-ST", "ONCE", "HOLD"};
+
+ enum MidiFXRepeatPages
+ {
+ MFXREPEATPAGE_CHANCE, // Chance Perc
+ MFXREPEATPAGE_MODERATE, // Mode, Rate, RateHz, Gate
+ MFXREPEATPAGE_QUANT, // Quant Rate
+ MFXREPEATPAGE_FADEVEL,
+ MFXREPEATPAGE_FADERATE
+ };
+
+ MidiFXRepeat::MidiFXRepeat()
+ {
+ params_.addPage(1); // MFXREPEATPAGE_CHANCE
+ params_.addPage(4); // MFXREPEATPAGE_MODERATE
+ params_.addPage(4); // MFXREPEATPAGE_QUANT
+ params_.addPage(4); // MFXREPEATPAGE_FADEVEL
+ params_.addPage(4); // MFXREPEATPAGE_FADERATE
+
+ chancePerc_ = 100;
+
+ mode_ = MFXREPEATMODE_ON;
+ numOfRepeats_ = 3;
+ rateIndex_ = 6;
+ quantizedRateIndex_ = -1; // Use global
+ rateHz_ = 100;
+ gate_ = 90;
+ velStart_ = 50;
+ velEnd_ = 100;
+ rateStart_ = 6;
+ rateEnd_ = 6;
+ rateStartHz_ = 100;
+ rateEndHz_ = 100;
+
+ fadeVel_ = false;
+ fadeRate_ = false;
+
+ quantizeSync_ = quantizedRateIndex_ >= -1; // -2 for off
+
+ noteMaster.setContext(this);
+ noteMaster.setProcessNoteFptr(&processNoteForwarder);
+ noteMaster.setSendNoteOutFptr(&sendNoteOutForwarder);
+
+ recalcVariables();
+ resync();
+
+ changeRepeatMode(MFXREPEATMODE_ON);
+
+ encoderSelect_ = true;
+ }
+
+ MidiFXRepeat::~MidiFXRepeat()
+ {
+ if (seqRunning_)
+ {
+ seqConfig.numOfActiveArps--;
+ }
+ }
+
+ int MidiFXRepeat::getFXType()
+ {
+ return MIDIFX_REPEAT;
+ }
+
+ const char *MidiFXRepeat::getName()
+ {
+ return "Repeat";
+ }
+
+ const char *MidiFXRepeat::getDispName()
+ {
+ return "RPT";
+ }
+
+ MidiFXInterface *MidiFXRepeat::getClone()
+ {
+ auto clone = new MidiFXRepeat();
+
+ clone->chancePerc_ = chancePerc_;
+ clone->numOfRepeats_ = numOfRepeats_;
+ clone->mode_ = mode_;
+ clone->rateIndex_ = rateIndex_;
+ clone->quantizedRateIndex_ = quantizedRateIndex_;
+ clone->rateHz_ = rateHz_;
+ clone->gate_ = gate_;
+ clone->velStart_ = velStart_;
+ clone->velEnd_ = velEnd_;
+
+ clone->fadeVel_ = fadeVel_;
+ clone->fadeRate_ = fadeRate_;
+
+ clone->rateStart_ = rateStart_;
+ clone->rateEnd_ = rateEnd_;
+
+ clone->rateStartHz_ = rateStartHz_;
+ clone->rateEndHz_ = rateEndHz_;
+
+ clone->recalcVariables();
+
+ return clone;
+ }
+
+ void MidiFXRepeat::onEnabled()
+ {
+ }
+
+ void MidiFXRepeat::onDisabled()
+ {
+ }
+
+ void MidiFXRepeat::noteInput(MidiNoteGroup note)
+ {
+ if (mode_ == MFXREPEATMODE_OFF)
+ {
+ // Serial.println("MFXREPEATMODE_OFF");
+ sendNoteOut(note);
+ return;
+ }
+
+ if (chancePerc_ != 100 && (chancePerc_ == 0 || random(100) > chancePerc_))
+ {
+ noteMaster.trackNoteInputPassthrough(¬e);
+ // // sendNoteOut(note);
+ // if(note.unknownLength || note.noteOff)
+ // {
+ // trackNoteInputPassthrough(¬e, false);
+ // }
+ // else
+ // {
+ // sendNoteOut(note);
+ // }
+
+ return;
+ }
+
+
+ noteMaster.trackNoteInput(¬e);
+
+ // if (note.unknownLength || note.noteOff)
+ // {
+ // // only notes of unknown lengths need to be tracked
+ // // notes with fixed lengths will turn off automatically.
+ // trackNoteInputPassthrough(¬e, true);
+ // trackNoteInput(¬e);
+ // }
+ // else
+ // {
+ // processNoteInput(¬e);
+ // }
+ }
+
+ // If chance is less than 100% and passing through, notes need to be tracked
+ // and if the same note comes in without passthrough for a noteoff event, it needs to
+ // be passed through the effect to send noteoff to prevent stuck notes
+ // void MidiFXRepeat::trackNoteInputPassthrough(MidiNoteGroup *note, bool ignoreNoteOns)
+ // {
+ // // Serial.println("trackNoteInputPassthrough");
+ // // Note on, not ignored
+ // if (!ignoreNoteOns && !note->noteOff)
+ // {
+ // // Serial.println("trackNoteInputPassthrough note on");
+
+ // // Search for an empty slot in trackingNoteGroupsPassthrough
+ // // If no slots are available/more than 8 notes/ note gets killed.
+ // for (uint8_t i = 0; i < 8; i++)
+ // {
+ // // Found empty slot
+ // if (trackingNoteGroupsPassthrough[i].prevNoteNumber == 255)
+ // {
+ // trackingNoteGroupsPassthrough[i].channel = note->channel;
+ // trackingNoteGroupsPassthrough[i].prevNoteNumber = note->prevNoteNumber;
+ // trackingNoteGroupsPassthrough[i].noteNumber = note->noteNumber;
+
+ // // Send it forward through chain
+ // sendNoteOut(*note);
+ // return;
+ // }
+ // }
+ // }
+
+ // // Note off
+ // if (note->noteOff)
+ // {
+ // // Serial.println("trackNoteInputPassthrough note off");
+
+ // // bool noteFound = false;
+
+ // // Search to see if this note is in trackingNoteGroupsPassthrough
+ // // Meaning it was previously passed through
+ // // If it is found, send it through chain
+ // // PrevNoteNumber should be the origin note number before being modified by MidiFX
+ // for (uint8_t i = 0; i < 8; i++)
+ // {
+ // if (trackingNoteGroupsPassthrough[i].prevNoteNumber != 255)
+ // {
+ // if (trackingNoteGroupsPassthrough[i].channel == note->channel && trackingNoteGroupsPassthrough[i].prevNoteNumber == note->prevNoteNumber)
+ // {
+ // note->noteNumber = trackingNoteGroupsPassthrough[i].noteNumber;
+ // // processNoteInput(note);
+ // sendNoteOut(*note);
+ // trackingNoteGroupsPassthrough[i].prevNoteNumber = 255; // mark empty
+ // // noteFound = true;
+ // }
+ // }
+ // }
+
+ // // Should be false if note getting sent to arp
+ // // Avoid double trackNoteInput call
+ // if(!ignoreNoteOns)
+ // {
+ // trackNoteInput(note);
+ // }
+ // }
+ // }
+
+ // void MidiFXRepeat::trackNoteInput(MidiNoteGroup *note)
+ // {
+ // // Same implementation with more comments in submode_midifx
+ // // Keeps track of previous note ons and and adjusts note number
+ // // for note offs using the prevNoteNumber parameter.
+ // // Why is this necessary?
+ // // If the note is modified by midifx like randomize before the arp
+ // // Then the arp can end up having notes stuck on
+ // // This ensures that notes don't get stuck on.
+ // if (note->noteOff)
+ // {
+ // bool noteFound = false;
+
+ // for (uint8_t i = 0; i < 8; i++)
+ // {
+ // if (trackingNoteGroups[i].prevNoteNumber != 255)
+ // {
+ // if (trackingNoteGroups[i].channel == note->channel && trackingNoteGroups[i].prevNoteNumber == note->prevNoteNumber)
+ // {
+ // // Serial.println("trackNoteInput note off found in trackingNoteGroups");
+ // note->noteNumber = trackingNoteGroups[i].noteNumber;
+ // processNoteInput(note);
+ // trackingNoteGroups[i].prevNoteNumber = 255; // mark empty
+ // noteFound = true;
+ // }
+ // }
+ // }
+
+ // if (!noteFound)
+ // {
+ // // Serial.println("trackNoteInput note off not found in trackingNoteGroups");
+
+ // processNoteInput(note);
+ // }
+ // }
+ // else if (!note->noteOff) // Note on
+ // {
+ // for (uint8_t i = 0; i < 8; i++)
+ // {
+ // // Find empty slot
+ // if (trackingNoteGroups[i].prevNoteNumber == 255)
+ // {
+ // trackingNoteGroups[i].channel = note->channel;
+ // trackingNoteGroups[i].prevNoteNumber = note->prevNoteNumber;
+ // trackingNoteGroups[i].noteNumber = note->noteNumber;
+
+ // processNoteInput(note);
+ // return;
+ // }
+ // }
+ // }
+ // }
+
+ void MidiFXRepeat::processNoteInput(MidiNoteGroup *note)
+ {
+ // Unknown length notes, played by human on keyboard
+ if (note->unknownLength)
+ {
+ if (note->noteOff)
+ {
+ repeatNoteOff(note);
+ }
+ else
+ {
+ repeatNoteOn(note);
+ }
+ }
+ // Fixed length notes, generated by sequencer, arp or MFX Repeat
+ else //
+ {
+ bool canInsert = true;
+
+ if (fixedLengthNotes.size() < queueSize)
+ {
+ for (uint8_t i = 0; i < fixedLengthNotes.size(); i++)
+ {
+ FixedLengthNote f = fixedLengthNotes[i];
+
+ // Note already exists
+ if (f.noteCache.noteNumber == note->noteNumber && f.noteCache.channel == note->channel)
+ {
+ // TODO: This should actually probably send a note off, then a note on for this note
+
+ // Update note off time
+ fixedLengthNotes[i].offTime = seqConfig.currentFrameMicros + (note->stepLength * clockConfig.step_micros);
+ canInsert = false;
+ break;
+ }
+ }
+ }
+ else
+ {
+ canInsert = false;
+ }
+
+ if (canInsert)
+ {
+ // Serial.println("Inserting fixed length note");
+ // Insert the note into the queue, calculate when it should turn off
+ // And send it through the repeat on
+ FixedLengthNote fixedNote;
+ fixedNote.noteCache.setFromNoteGroup(note);
+ fixedNote.offTime = seqConfig.currentFrameMicros + (note->stepLength * clockConfig.step_micros);
+ fixedLengthNotes.push_back(fixedNote);
+ repeatNoteOn(note);
+ }
+ else
+ {
+ // Remove from tracking notes
+ noteMaster.removeFromTracking(note);
+ // for (uint8_t i = 0; i < 8; i++)
+ // {
+ // if (trackingNoteGroups[i].prevNoteNumber != 255)
+ // {
+ // if (trackingNoteGroups[i].channel == note->channel && trackingNoteGroups[i].prevNoteNumber == note->prevNoteNumber)
+ // {
+ // trackingNoteGroups[i].prevNoteNumber = 255; // mark empty
+ // }
+ // }
+ // }
+ // Too many notes, note gets killed.
+ // sendNoteOut(*note);
+ }
+ }
+ }
+
+ bool MidiFXRepeat::hasMidiNotes()
+ {
+ return playedNoteQueue.size() > 0;
+ }
+
+ bool MidiFXRepeat::useRateHz()
+ {
+ return rateIndex_ < 0;
+ }
+
+ float MidiFXRepeat::rateToHz(uint8_t rateHz)
+ {
+ float hertz = 1.0f;
+
+ if (rateHz < 100)
+ {
+ hertz = map((float)rateHz, 0.0f, 100.0f, 0.1f, 1.0f);
+ }
+ else if (rateHz == 100)
+ {
+ hertz = 1.0f;
+ }
+ else if (rateHz > 100)
+ {
+ hertz = map((float)rateHz, 100.0f, 255.0f, 1.0f, 50.0f);
+ }
+
+ return hertz;
+ }
+
+
+ void MidiFXRepeat::updateMultiplier()
+ {
+ if (!multiplierCalculated_)
+ {
+ // Use Hertz
+ if (useRateHz())
+ {
+ multiplier_ = 1;
+
+ rateInHz_ = rateToHz(rateHz_);
+ rateStartInHz_ = rateToHz(rateStartHz_);
+ rateEndInHz_ = rateToHz(rateEndHz_);
+ }
+ else
+ {
+ uint8_t rate = kArpRates[rateIndex_]; // 8
+ // uint8_t rate = 16; // 8
+ multiplier_ = 1.0f / (float)rate; // 1 / 8 = 0.125 // Only need to recalculate this if rate changes yo
+
+ rateStartMult_ = 1.0f / ((float)kArpRates[rateStart_]);
+ rateEndMult_ = 1.0f / ((float)kArpRates[rateEnd_]);
+ }
+
+ multiplierCalculated_ = true;
+ }
+ }
+
+ bool MidiFXRepeat::insertMidiNoteQueue(MidiNoteGroup *note)
+ {
+ // Serial.println("insertMidiNoteQueue");
+ // Serial.println("playedNoteQueue.capacity(): " + String(playedNoteQueue.capacity()));
+ // Serial.println("activeNoteQueue.capacity(): " + String(activeNoteQueue.capacity()));
+ // Serial.println("pendingNoteQueue.capacity(): " + String(pendingNoteQueue.capacity()));
+ // Serial.println("tempNoteQueue.capacity(): " + String(tempNoteQueue.capacity()));
+ // Serial.println("fixedLengthNotes.capacity(): " + String(fixedLengthNotes.capacity()));
+
+ if (playedNoteQueue.capacity() > queueSize)
+ {
+ playedNoteQueue.shrink_to_fit();
+ }
+
+ bool noteAdded = false;
+
+ // Played note queue simply tracks which notes are being played.
+ // These notes do not get played
+ if (playedNoteQueue.size() < queueSize)
+ {
+ playedNoteQueue.push_back(RepeatNote(note));
+ noteAdded = true;
+ }
+
+ if (activeNoteQueue.capacity() > queueSize)
+ {
+ activeNoteQueue.shrink_to_fit();
+ }
+
+ if (pendingNoteQueue.capacity() > queueSize)
+ {
+ pendingNoteQueue.shrink_to_fit();
+ }
+
+ // if(!noteAdded)
+ // {
+ // Serial.println("Could not add note");
+ // }
+
+ // Room up to 16 in playedNoteQueue, can add to pending or active
+ if (noteAdded)
+ {
+ // In these modes the note will be on
+ // Remove the note and readd to avoid weird overlapping repeats
+ if (mode_ == MFXREPEATMODE_1SHOT || mode_ == MFXREPEATMODE_HOLD)
+ {
+ removeFromQueue(&activeNoteQueue, note);
+ removeFromQueue(&pendingNoteQueue, note);
+ }
+
+ if (activeNoteQueue.size() + pendingNoteQueue.size() < queueSize)
+ {
+ // Add to pending queue, note will be added to active queue on clock
+ if (quantizeSync_)
+ {
+ // Serial.println("adding quantize note");
+
+ auto newNote = RepeatNote(note);
+
+ newNote.repeatCounter = numOfRepeats_ + 1;
+
+ newNote.velocityStart = note->velocity * velStartPerc_;
+ newNote.velocityEnd = note->velocity * velEndPerc_;
+
+ newNote.nextTriggerTime = seqConfig.lastClockMicros;
+
+ pendingNoteQueue.push_back(newNote);
+ }
+ // No quantization, play immediately by adding to active note queue
+ else
+ {
+ // Serial.println("adding non-quantize note");
+
+ auto newNote = RepeatNote(note);
+
+ newNote.repeatCounter = numOfRepeats_ + 1;
+
+ newNote.velocityStart = note->velocity * velStartPerc_;
+ newNote.velocityEnd = note->velocity * velEndPerc_;
+
+ newNote.nextTriggerTime = seqConfig.lastClockMicros;
+
+ activeNoteQueue.push_back(newNote);
+ }
+ }
+ }
+
+ // Serial.println("Note Added: " + String(noteAdded));
+ return noteAdded;
+ }
+
+ bool MidiFXRepeat::removeFromQueue(std::vector *queue, MidiNoteGroup *note)
+ {
+ bool foundNoteToRemove = false;
+
+ if (queue->size() > 0)
+ {
+ auto it = queue->begin();
+ while (it != queue->end())
+ {
+ // Serial.println("activeNoteQueue: " + String(it->noteNumber) + " Chan: " + String(it->channel));
+
+ if (it->noteNumber == note->noteNumber && it->channel == note->channel - 1)
+ {
+ // `erase()` invalidates the iterator, use returned iterator
+ it = queue->erase(it);
+ foundNoteToRemove = true;
+ }
+ else
+ {
+ ++it;
+ }
+ }
+ }
+
+ return foundNoteToRemove;
+ }
+
+
+ bool MidiFXRepeat::removeMidiNoteQueue(MidiNoteGroup *note)
+ {
+ bool foundNoteToRemove = false;
+
+ // Always remove from played notes
+ if(removeFromQueue(&playedNoteQueue, note))
+ {
+ foundNoteToRemove = true;
+ }
+
+ switch (mode_)
+ {
+ // Should not get here
+ case MFXREPEATMODE_OFF:
+ break;
+ case MFXREPEATMODE_ON:
+ {
+ removeFromQueue(&activeNoteQueue, note);
+ removeFromQueue(&pendingNoteQueue, note);
+ }
+ break;
+ // Don't remove from active or pending
+ // each note gets removed after playing for number of times
+ case MFXREPEATMODE_1SHOT:
+ break;
+ // Don't remove from active or pending
+ // queue will be reset once playedNoteQueue is empty and a new noteon comes in
+ case MFXREPEATMODE_HOLD:
+ break;
+ }
+
+ return foundNoteToRemove;
+ }
+
+ void MidiFXRepeat::changeRepeatMode(uint8_t newMode)
+ {
+ uint8_t prevMode = mode_;
+ mode_ = newMode;
+
+ if ((mode_ == MFXREPEATMODE_ON && hasMidiNotes() == false) || (mode_ == MFXREPEATMODE_ONCE && hasMidiNotes() == false) || mode_ == MFXREPEATMODE_OFF)
+ {
+ stopSeq();
+ }
+
+ switch (mode_)
+ {
+ case MFXREPEATMODE_OFF:
+ case MFXREPEATMODE_ON:
+ resync();
+ break;
+ }
+
+ if(prevMode != newMode && newMode == MFXREPEATMODE_OFF)
+ {
+ seqRunning_ = false;
+ recalcVariables();
+ resync();
+ }
+ }
+
+ void MidiFXRepeat::resync()
+ {
+ playedNoteQueue.clear();
+ tempNoteQueue.clear();
+ activeNoteQueue.clear();
+ pendingNoteQueue.clear();
+ fixedLengthNotes.clear();
+
+ resetArpSeq();
+
+ noteMaster.clear();
+
+ // for (uint8_t i = 0; i < 8; i++)
+ // {
+ // trackingNoteGroups[i].prevNoteNumber = 255;
+ // trackingNoteGroupsPassthrough[i].prevNoteNumber = 255;
+ // }
+ }
+
+ void MidiFXRepeat::repeatNoteOn(MidiNoteGroup *note)
+ {
+ // Serial.println("repeatNoteOn");
+
+ // bool seqReset = false;
+
+ if (!seqRunning_)
+ {
+ startSeq();
+ resetArpSeq();
+ // seqReset = true;
+
+ // Serial.println("playedNoteQueue: " + String(playedNoteQueue.size()));
+ // Serial.println("activeNoteQueue: " + String(activeNoteQueue.size()));
+ // Serial.println("pendingNoteQueue: " + String(pendingNoteQueue.size()));
+ // Serial.println("tempNoteQueue: " + String(tempNoteQueue.size()));
+ // Serial.println("fixedLengthNotes: " + String(fixedLengthNotes.size()));
+ }
+
+ if (hasMidiNotes() == false)
+ {
+ // Serial.println("No Midi Notes");
+
+
+
+ // velocity_ = note.velocity;
+ // sendMidi_ = note.sendMidi;
+ // sendCV_ = note.sendCV;
+ // midiChannel_ = note.channel - 1; // note.channel is 1-16, sub 1 for 0-15
+
+
+ // resetArpSeq();
+ // seqReset = true;
+
+ // For one shot mode, notes are removed from active
+ // once they have played a certain number of times
+ // Important to clear this for hold mode
+ if (mode_ != MFXREPEATMODE_1SHOT)
+ {
+ // Reset the active note queue
+ activeNoteQueue.clear();
+ }
+ }
+
+ // Serial.println("seqRunning_: " + String(seqRunning_));
+
+ // else
+ // {
+ // // if (resetMode_ == ARPRESET_NOTE)
+ // // {
+ // // resetArpSeq();
+ // // seqReset = true;
+ // // }
+ // }
+
+ insertMidiNoteQueue(note);
+ // sortNotes();
+
+ // if (seqReset)
+ // {
+ // // nextNotePos_ = notePos_;
+ // // prevQLength_ = sortedNoteQueue.size();
+ // }
+
+ // if (pendingStop_)
+ // {
+ // pendingStop_ = false;
+ // }
+
+ // if (!seqReset && !pendingStart_)
+ // {
+ // findIndexOfNextNotePos();
+ // }
+ }
+ void MidiFXRepeat::repeatNoteOff(MidiNoteGroup *note)
+ {
+ // Serial.println("repeatNoteOff");
+ removeMidiNoteQueue(note);
+ // sortNotes();
+
+ if ((mode_ == MFXREPEATMODE_ON || mode_ == MFXREPEATMODE_ONCE) && hasMidiNotes() == false)
+ {
+ stopSeq();
+ }
+ // if (hasMidiNotes())
+ // {
+ // findIndexOfNextNotePos();
+ // }
+ }
+
+ void MidiFXRepeat::startSeq()
+ {
+ // Serial.println("startArp");
+ if (seqRunning_)
+ return;
+
+ // pendingStart_ = true;
+ // sortOrderChanged_ = false;
+ // resetNextTrigger_ = false;
+
+ // pendingStartTime_ = micros();
+
+ // notePos_ = 0;
+ // prevNotePos_ = 0;
+ // nextNotePos_ = 0;
+
+ // if (omxUtil.areClocksRunning() == false)
+ // {
+ // pendingStart_ = true;
+ // }
+ // else
+ // {
+ // doPendingStart();
+ // }
+
+ updateMultiplier();
+
+ if(seqConfig.numOfActiveArps <= 0)
+ {
+ // omxUtil.resetPPQCounter();
+ }
+
+ // if (omxUtil.areClocksRunning() == false)
+ // {
+ // omxUtil.restartClocks();
+ // omxUtil.startClocks();
+ // // uint8_t rate = kArpRates[rateIndex_];
+ // // multiplier_ = 1.0f / (float)rate;
+ // stepMicroDelta_ = (clockConfig.step_micros * 16) * multiplier_;
+ // nextStepTimeP_ = seqConfig.lastClockMicros; // Should be current time, start now.
+ // nextRepeatTriggerTime_ = nextStepTimeP_;
+
+ // next16thTime_ = seqConfig.lastClockMicros;
+ // last16thTime_ = seqConfig.lastClockMicros;
+ // }
+ // else
+ // {
+ // nextStepTimeP_ = nextRepeatTriggerTime_;
+ // }
+
+ // lastStepTimeP_ = nextStepTimeP_;
+
+ seqRunning_ = true;
+ // pendingStart_ = false;
+ // pendingStop_ = false;
+
+ seqConfig.numOfActiveArps++;
+ }
+
+ void MidiFXRepeat::stopSeq()
+ {
+ // pendingStart_ = false;
+ // pendingStop_ = false;
+ // arpRunning_ = false;
+ // pendingStopCount_ = 0;
+
+ // doPendingStop();
+
+ if (seqRunning_)
+ {
+ // Stop clocks if last arp
+ seqConfig.numOfActiveArps--;
+ // if (seqConfig.numOfActiveArps <= 0)
+ // {
+ // omxUtil.stopClocks();
+ // }
+ }
+
+ seqRunning_ = false;
+ // pendingStart_ = false;
+ // pendingStop_ = false;
+
+ // // Serial.println("stopArp");
+ // arpRunning_ = false;
+ // pendingStart_ = false;
+ }
+
+ void MidiFXRepeat::resetArpSeq()
+ {
+ // Serial.println("resetArpSeq");
+ // patPos_ = 0;
+ // transpPos_ = 0;
+ // modPos_ = 0;
+ // notePos_ = 0;
+ // octavePos_ = 0;
+ // syncPos_ = 0;
+
+ // lastPlayedNoteNumber_ = -127;
+
+ // randPrevNote_ = 255;
+
+ // goingUp_ = true;
+ // resetNextTrigger_ = false;
+
+ // prevNotePos_ = 0;
+ // nextNotePos_ = 0;
+ }
+
+ // void MidiFXRepeat::sortNotes()
+ // {
+ // activeNoteQueue.clear();
+
+ // // Copy played or held notes to sorted note queue
+ // if (mode_ != MFXREPEATMODE_ON && mode_ != MFXREPEATMODE_ONCE)
+ // {
+ // for (RepeatNote a : holdNoteQueue)
+ // {
+ // activeNoteQueue.push_back(a);
+ // }
+ // }
+ // else
+ // {
+ // for (RepeatNote a : playedNoteQueue)
+ // {
+ // activeNoteQueue.push_back(a);
+ // }
+ // }
+
+ // if (activeNoteQueue.size() == 0)
+ // return; // Not much to do without any notes
+
+ // // Keep vectors in check
+ // if (activeNoteQueue.capacity() > queueSize)
+ // {
+ // activeNoteQueue.shrink_to_fit();
+ // }
+
+ // if (tempNoteQueue.capacity() > queueSize)
+ // {
+ // tempNoteQueue.shrink_to_fit();
+ // }
+ // }
+
+ void MidiFXRepeat::triggerNote(RepeatNote note)
+ {
+ if(note.noteNumber > 127) return;
+
+ playNote(seqConfig.currentFrameMicros, note.nextTriggerDelta, note.noteNumber, note.velocity, note.channel);
+ }
+
+ // void MidiFXRepeat::repeatNoteTrigger()
+ // {
+ // // Serial.println("repeatNoteTrigger");
+
+ // if (activeNoteQueue.size() == 0)
+ // {
+ // // Serial.println("no sorted notes");
+
+ // return;
+ // }
+
+ // uint32_t noteon_micros = seqConfig.currentFrameMicros;
+
+ // // if (resetNextTrigger_)
+ // // {
+ // // resetArpSeq();
+ // // }
+
+ // // bool incrementOctave = false;
+ // // int currentNotePos = notePos_;
+ // // int nextNotePos = notePos_;
+ // // int qLength = sortedNoteQueue.size();
+
+ // // prevNotePos_ = notePos_;
+
+ // // prevQLength_ = qLength;
+
+ // // syncPos_ = syncPos_ + 1 % 16;
+
+ // // currentNotePos = constrain(currentNotePos, 0, qLength - 1);
+
+ // for (RepeatNote r : activeNoteQueue)
+ // {
+ // if (r.noteNumber >= 0 && r.noteNumber <= 127)
+ // {
+ // playNote(noteon_micros, r.noteNumber, r.velocity, r.channel);
+ // }
+ // }
+
+ // // ArpNote arpNote = sortedNoteQueue[currentNotePos];
+ // // randPrevNote_ = arpNote.noteNumber;
+
+ // // int16_t noteNumber = arpNote.noteNumber;
+
+ // // noteNumber = applyModPattern(noteNumber, arpNote.channel);
+ // // stepLength_ = findStepLength(); // Can be changed by ties in mod pattern
+
+ // // if (noteNumber != -127)
+ // // {
+ // // noteNumber = applyTranspPattern(noteNumber);
+
+ // // // Add octave
+ // // noteNumber = noteNumber + (octavePos_ * octDistance_);
+ // // playNote(noteon_micros, noteNumber, velocity_, arpNote.channel);
+ // // }
+
+ // // bool seqReset = false;
+
+ // // // Advance mod pattern
+ // // modPos_++;
+ // // if (modPos_ >= modPatternLength_ + 1)
+ // // {
+ // // if (resetMode_ == ARPRESET_MODPAT)
+ // // {
+ // // resetArpSeq();
+ // // seqReset = true;
+ // // }
+ // // modPos_ = 0;
+ // // }
+
+ // // // Advance transpose pattern
+ // // transpPos_++;
+ // // if (transpPos_ >= transpPatternLength_ + 1)
+ // // {
+ // // if (resetMode_ == ARPRESET_TRANSPOSEPAT)
+ // // {
+ // // resetArpSeq();
+ // // seqReset = true;
+ // // }
+ // // transpPos_ = 0;
+ // // }
+
+ // // if (!seqReset)
+ // // {
+ // // notePos_ = nextNotePos;
+
+ // // nextNotePos_ = (notePos_ + qLength) % qLength;
+ // // }
+ // // else
+ // // {
+ // // nextNotePos_ = notePos_;
+ // // }
+
+ // // prevSortedNoteQueue.clear();
+
+ // // for (ArpNote a : sortedNoteQueue)
+ // // {
+ // // prevSortedNoteQueue.push_back(a);
+ // // }
+
+ // // playNote(noteon_micros, notePat_[patPos_]);
+
+ // // patPos_++;
+ // }
+
+ void MidiFXRepeat::playNote(uint32_t noteOnMicros, uint32_t lengthDelta, int16_t noteNumber, uint8_t velocity, uint8_t channel)
+ {
+ // Serial.println("SeqRunning: " + String(seqRunning_));
+ // Serial.println("PlayNote: " + String(noteNumber) + " " + String(velocity) + " " + String(channel));
+ if (noteNumber < 0 || noteNumber > 127)
+ return;
+
+ MidiNoteGroup noteOut;
+
+ noteOut.channel = channel + 1;
+ noteOut.noteNumber = (uint8_t)noteNumber;
+ noteOut.prevNoteNumber = (uint8_t)noteNumber;
+ noteOut.velocity = velocity;
+ // noteOut.stepLength = ((float)gate_ * 0.01f) * (16.0f * multiplier_);
+
+ noteOut.stepLength = (lengthDelta * ((float)gate_ * 0.01f)) / clockConfig.step_micros;
+
+ // Serial.println("stepLength: " + String(noteOut.stepLength));
+
+ // noteOut.stepLength = ((float)gate_ * 0.01f) * (16.0f * multiplier_);
+
+ // noteOut.sendMidi = sendMidi_;
+ // noteOut.sendCV = sendCV_;
+
+ noteOut.sendMidi = true;
+ noteOut.sendCV = true;
+ noteOut.noteonMicros = noteOnMicros;
+ noteOut.unknownLength = false;
+ noteOut.noteOff = false;
+
+ // lastPlayedNoteNumber_ = noteNumber;
+
+ sendNoteOut(noteOut);
+ }
+
+ void MidiFXRepeat::onClockTick()
+ {
+ if(pendingNoteQueue.size() == 0) return;
+
+ uint8_t quantIndex = quantizedRateIndex_ < 0 ? clockConfig.globalQuantizeStepIndex : quantizedRateIndex_; // Use global or local quantize rate?
+
+ bool isQuantizedStep = seqConfig.currentClockTick % (96 * 4 / kArpRates[quantIndex]) == 0;
+
+ // Move pending notes to active
+ if(isQuantizedStep)
+ {
+ for (uint8_t i = 0; i < pendingNoteQueue.size(); i++)
+ {
+ pendingNoteQueue[i].nextTriggerTime = seqConfig.lastClockMicros;
+ activeNoteQueue.push_back(pendingNoteQueue[i]);
+ }
+
+ pendingNoteQueue.clear();
+ }
+ }
+
+ void MidiFXRepeat::loopUpdate()
+ {
+ auto now = seqConfig.currentFrameMicros;
+
+ // Send arp offs for notes that had fixed lengths
+ auto it = fixedLengthNotes.begin();
+ while (it != fixedLengthNotes.end())
+ {
+ // remove matching note numbers
+ if (it->offTime <= now)
+ {
+ auto noteGroup = it->noteCache.toMidiNoteGroup();
+
+ // Serial.println("Removing pending note");
+ repeatNoteOff(¬eGroup);
+ // `erase()` invalidates the iterator, use returned iterator
+ it = fixedLengthNotes.erase(it);
+ }
+ else
+ {
+ ++it;
+ }
+ }
+
+ if (!seqRunning_)
+ {
+ return;
+ }
+
+ if (sysSettings.omxMode == MODE_MIDI && !selected_)
+ {
+ // Serial.println("Not selected");
+ return;
+ }
+
+ updateMultiplier();
+
+ uint32_t stepmicros = seqConfig.currentFrameMicros;
+
+ tempNoteQueue.clear();
+
+ // Loop through all the notes and see which notes should be triggered this frame
+ // if a note should be triggered it gets added to the tempNoteQueue
+ for(uint8_t i = 0; i < activeNoteQueue.size(); i++)
+ {
+ // The time has come to
+ if(stepmicros >= activeNoteQueue[i].nextTriggerTime)
+ {
+ if(fadeVel_)
+ {
+ activeNoteQueue[i].velocity = map(activeNoteQueue[i].repeatCounter, 0, numOfRepeats_, activeNoteQueue[i].velocityEnd, activeNoteQueue[i].velocityStart);
+ }
+
+ if (useRateHz()) // Use hertz
+ {
+ float hzRate = fadeRate_ ? map((float)activeNoteQueue[i].repeatCounter, 0.0f, (float)numOfRepeats_, rateEndInHz_, rateStartInHz_) : rateInHz_;
+
+ activeNoteQueue[i].nextTriggerDelta = (1.0f / hzRate) * secs2micros;
+ }
+ else // Synced
+ {
+ float mult = fadeRate_ ? map((float)activeNoteQueue[i].repeatCounter, 0.0f, (float)numOfRepeats_, rateEndMult_, rateStartMult_) : multiplier_;
+
+ activeNoteQueue[i].nextTriggerDelta = clockConfig.step_micros * 16 * mult; // 1/8th note, 120 bpm, 124992 * 16 * 0.125 = 249984 / 124992 = 2 steps, 16 / 2 = 8
+ }
+
+ activeNoteQueue[i].nextTriggerTime = activeNoteQueue[i].nextTriggerTime + activeNoteQueue[i].nextTriggerDelta;
+
+ if(activeNoteQueue[i].repeatCounter > 0)
+ {
+ activeNoteQueue[i].repeatCounter -= 1;
+ }
+
+ // Don't hold back
+ tempNoteQueue.push_back(activeNoteQueue[i]);
+ }
+ }
+
+ // Trigger any notes that should be triggered this frame
+ for (RepeatNote n : tempNoteQueue)
+ {
+ triggerNote(n);
+ }
+
+ // Once and 1shot modes will use repeat counter
+ // if any of these counters reached 0, remove the note
+ if ((mode_ == MFXREPEATMODE_1SHOT || mode_ == MFXREPEATMODE_ONCE) && activeNoteQueue.size() > 0)
+ {
+ auto it = activeNoteQueue.begin();
+ while (it != activeNoteQueue.end())
+ {
+ if(it->repeatCounter <= 0)
+ {
+ it = activeNoteQueue.erase(it);
+ }
+ else
+ {
+ ++it;
+ }
+ }
+ }
+ }
+
+ void MidiFXRepeat::onEncoderChangedEditParam(Encoder::Update enc)
+ {
+ int8_t page = params_.getSelPage();
+ int8_t param = params_.getSelParam();
+
+ auto amtSlow = enc.accel(1);
+ auto amtFast = enc.accel(5);
+
+ switch (page)
+ {
+ case MFXREPEATPAGE_CHANCE:
+ {
+ switch (param)
+ {
+ case 0:
+ chancePerc_ = constrain(chancePerc_ + amtSlow, 0, 100);
+ break;
+ case 1:
+ break;
+ case 2:
+ break;
+ case 3:
+ break;
+ }
+ }
+ break;
+ case MFXREPEATPAGE_MODERATE:
+ {
+ switch (param)
+ {
+ case 0:
+ {
+ uint8_t newMode = constrain(mode_ + amtSlow, 0, MFXREPEATMODE_COUNT - 1);
+ changeRepeatMode(newMode);
+ }
+ break;
+ case 1:
+ rateIndex_ = constrain(rateIndex_ + amtSlow, -1, kNumArpRates - 1);
+ multiplierCalculated_ = false;
+ break;
+ case 2:
+ rateHz_ = constrain(rateHz_ + amtFast, 0, 255);
+ rateInHz_ = rateToHz(rateHz_);
+ multiplierCalculated_ = false;
+ break;
+ case 3:
+ gate_ = constrain(gate_ + amtFast, 2, 200);
+ break;
+ }
+ }
+ break;
+ case MFXREPEATPAGE_QUANT:
+ {
+ switch (param)
+ {
+ case 0:
+ quantizedRateIndex_ = constrain(quantizedRateIndex_ + amtSlow, -2, kNumArpRates - 1);
+ quantizeSync_ = quantizedRateIndex_ >= -1; // -2 for off
+ break;
+ case 1:
+ numOfRepeats_ = constrain(numOfRepeats_ + amtSlow, 0, 63);
+ break;
+ case 2:
+ break;
+ case 3:
+ break;
+ }
+ }
+ break;
+ case MFXREPEATPAGE_FADEVEL:
+ {
+ switch (param)
+ {
+ case 0:
+ fadeVel_ = constrain(fadeVel_ + amtSlow, 0, 1);
+ break;
+ case 1:
+ velStart_ = constrain(velStart_ + amtFast, 0, 100);
+ velStartPerc_ = velStart_ / 100.0f;
+ break;
+ case 2:
+ velEnd_ = constrain(velEnd_ + amtFast, 0, 100);
+ velEndPerc_ = velEnd_ / 100.0f;
+ break;
+ case 3:
+ break;
+ }
+ }
+ break;
+ case MFXREPEATPAGE_FADERATE:
+ {
+ switch (param)
+ {
+ case 0:
+ fadeRate_ = constrain(fadeRate_ + amtSlow, 0, 1);
+ break;
+ case 1:
+ if (useRateHz())
+ {
+ rateStartHz_ = constrain(rateStartHz_ + amtFast, 0, 255);
+ }
+ else
+ {
+ rateStart_ = constrain(rateStart_ + amtSlow, 0, kNumArpRates - 1);
+ }
+ multiplierCalculated_ = false;
+ break;
+ case 2:
+ if (useRateHz())
+ {
+ rateEndHz_ = constrain(rateEndHz_ + amtFast, 0, 255);
+ }
+ else
+ {
+ rateEnd_ = constrain(rateEnd_ + amtSlow, 0, kNumArpRates - 1);
+ }
+ multiplierCalculated_ = false;
+ break;
+ case 3:
+ break;
+ }
+ }
+ break;
+ }
+
+ omxDisp.setDirty();
+ }
+
+ // used 186352 bytes
+ // 186320 bytes
+
+ void MidiFXRepeat::onDisplayUpdate(uint8_t funcKeyMode)
+ {
+ omxDisp.clearLegends();
+
+ int8_t page = params_.getSelPage();
+
+ if(page == MFXREPEATPAGE_CHANCE)
+ {
+ omxDisp.dispParamBar(chancePerc_, chancePerc_, 0, 100, !getEncoderSelect(), false, "Repeat", "Chance");
+ return;
+ }
+
+ switch (page)
+ {
+ case MFXREPEATPAGE_MODERATE:
+ {
+ omxDisp.setLegend(0, "MODE", kRepeatModeDisp_[mode_]);
+ omxDisp.setLegend(1, "RATE", rateIndex_ < 0 ? ("HZ") : ("1/" + String(kArpRates[rateIndex_])));
+ omxDisp.setLegend(2, "RTHZ", rateInHz_ < 1.0f ? (String(rateInHz_, 2)) : (String(rateInHz_, 1)));
+ omxDisp.setLegend(3, "Gate", gate_);
+ }
+ break;
+ case MFXREPEATPAGE_QUANT:
+ {
+ omxDisp.setLegend(0, "QUANT", quantizedRateIndex_ <= -2, quantizedRateIndex_ == -1 ? ("GBL") : ("1/" + String(kArpRates[quantizedRateIndex_])));
+ omxDisp.setLegend(1, "#RPT", numOfRepeats_ + 1);
+ }
+ break;
+ case MFXREPEATPAGE_FADEVEL:
+ {
+ omxDisp.setLegend(0, "FVEL", !fadeVel_, "FADE");
+ omxDisp.setLegend(1, "STRT", velStart_);
+ omxDisp.setLegend(2, "END", velEnd_);
+ }
+ break;
+ case MFXREPEATPAGE_FADERATE:
+ {
+ omxDisp.setLegend(0, "FRAT", !fadeRate_, "FADE");
+
+ if(useRateHz())
+ {
+ float sHz = rateToHz(rateStartHz_);
+ float eHz = rateToHz(rateEndHz_);
+
+ omxDisp.setLegend(1, "STRT", sHz < 1.0f ? String(sHz, 2) : String(sHz, 1));
+ omxDisp.setLegend(2, "END", eHz < 1.0f ? String(eHz, 2) : String(eHz, 1));
+ }
+ else
+ {
+ omxDisp.setLegend(1, "STRT", "1/" + String(kArpRates[rateStart_]));
+ omxDisp.setLegend(2, "END", "1/" + String(kArpRates[rateEnd_]));
+ }
+ }
+ break;
+ }
+
+ omxDisp.dispGenericMode2(params_.getNumPages(), params_.getSelPage(), params_.getSelParam(), getEncoderSelect());
+ }
+
+ int MidiFXRepeat::saveToDisk(int startingAddress, Storage *storage)
+ {
+ RepeatSave save;
+ save.chancePerc = chancePerc_;
+ save.numOfRepeats = numOfRepeats_;
+ save.mode = mode_;
+ save.rateIndex = rateIndex_;
+ save.rateHz = rateHz_;
+ save.quantizedRateIndex_ = quantizedRateIndex_;
+ save.gate = gate_;
+ save.velStart = velStart_;
+ save.velEnd = velEnd_;
+ save.fadeVel_ = fadeVel_;
+ save.fadeRate_ = fadeRate_;
+ save.rateStart_ = rateStart_;
+ save.rateEnd_ = rateEnd_;
+ save.rateStartHz_ = rateStartHz_;
+ save.rateEndHz_ = rateEndHz_;
+
+ int saveSize = sizeof(RepeatSave);
+
+ auto saveBytesPtr = (byte *)(&save);
+ for (int j = 0; j < saveSize; j++)
+ {
+ storage->write(startingAddress + j, *saveBytesPtr++);
+ }
+
+ return startingAddress + saveSize;
+ }
+
+ int MidiFXRepeat::loadFromDisk(int startingAddress, Storage *storage)
+ {
+ int saveSize = sizeof(RepeatSave);
+
+ auto save = RepeatSave{};
+ auto current = (byte *)&save;
+ for (int j = 0; j < saveSize; j++)
+ {
+ *current = storage->read(startingAddress + j);
+ current++;
+ }
+
+ chancePerc_ = save.chancePerc;
+ numOfRepeats_ = save.numOfRepeats;
+ mode_ = save.mode;
+ rateHz_ = save.rateHz;
+ rateIndex_ = save.rateIndex;
+ quantizedRateIndex_ = save.quantizedRateIndex_;
+ gate_ = save.gate;
+ fadeVel_ = save.fadeVel_;
+ fadeRate_ = save.fadeRate_;
+ velStart_ = save.velStart;
+ velEnd_ = save.velEnd;
+ rateStart_ = save.rateStart_;
+ rateEnd_ = save.rateEnd_;
+ rateStartHz_ = save.rateStartHz_;
+ rateEndHz_ = save.rateEndHz_;
+
+ recalcVariables();
+ resync();
+
+ return startingAddress + saveSize;
+ }
+
+ void MidiFXRepeat::recalcVariables()
+ {
+ rateInHz_ = rateToHz(rateHz_);
+
+ velStartPerc_ = velStart_ / 100.0f;
+ velEndPerc_ = velEnd_ / 100.0f;
+
+ quantizeSync_ = quantizedRateIndex_ >= -1; // -2 for off
+
+ multiplierCalculated_ = false;
+ updateMultiplier();
+ }
+}
diff --git a/Archive/OMX-27-firmware/src/midifx/midifx_repeat.h b/Archive/OMX-27-firmware/src/midifx/midifx_repeat.h
new file mode 100644
index 00000000..c1435c28
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/midifx/midifx_repeat.h
@@ -0,0 +1,211 @@
+#pragma once
+
+#include "midifx_interface.h"
+#include "midifx_notemaster.h"
+
+namespace midifx
+{
+
+ class MidiFXRepeat : public MidiFXInterface
+ {
+ public:
+ MidiFXRepeat();
+ ~MidiFXRepeat();
+
+ int getFXType() override;
+ const char *getName() override;
+ const char *getDispName() override;
+
+ MidiFXInterface *getClone() override;
+
+ void loopUpdate() override;
+ void onClockTick() override;
+ void resync() override;
+
+ void onDisplayUpdate(uint8_t funcKeyMode) override;
+
+ void noteInput(MidiNoteGroup note) override;
+
+ int saveToDisk(int startingAddress, Storage *storage) override;
+ int loadFromDisk(int startingAddress, Storage *storage) override;
+
+ protected:
+ void onEnabled() override;
+ void onDisabled() override;
+
+ void onEncoderChangedEditParam(Encoder::Update enc) override;
+
+ private:
+ struct FixedLengthNote
+ {
+ MidiNoteGroupCache noteCache;
+ Micros offTime;
+ };
+
+ struct RepeatNote
+ {
+ // bool playing; // Not needed now with two queues
+ // bool inUse = false;
+ uint8_t noteNumber;
+ uint8_t channel : 4;
+ uint8_t velocity : 7;
+ uint8_t velocityStart : 7;
+ uint8_t velocityEnd : 7;
+ uint8_t repeatCounter : 7;
+
+ // bool sendMidi = false;
+ // bool sendCV = false;
+
+ Micros nextTriggerDelta = 0; // Delta time to next trigger
+ Micros nextTriggerTime = 0; // Time in global microseconds when note should trigger next
+
+ RepeatNote()
+ {
+ noteNumber = 255;
+ velocity = 100;
+ channel = 0;
+ }
+
+ RepeatNote(int noteNumber, uint8_t velocity, uint8_t channel)
+ {
+ if (noteNumber < 0 || noteNumber > 127)
+ {
+ noteNumber = 255;
+ }
+ else
+ {
+ this->noteNumber = noteNumber;
+ this->velocity = velocity;
+ this->channel = channel - 1;
+ }
+ }
+
+ RepeatNote(MidiNoteGroup *noteGroup)
+ {
+ if (noteGroup->noteNumber < 0 || noteGroup->noteNumber > 127)
+ {
+ noteNumber = 255;
+ return;
+ }
+ this->noteNumber = noteGroup->noteNumber;
+ this->velocity = noteGroup->velocity;
+ this->channel = noteGroup->channel - 1;
+ }
+ };
+
+ struct RepeatSave
+ {
+ uint8_t chancePerc : 7;
+ uint8_t numOfRepeats : 6;
+ uint8_t mode : 3;
+ int8_t rateIndex : 5;
+ int8_t quantizedRateIndex_ : 5;
+ uint8_t rateHz;
+ uint8_t gate : 8;
+ bool fadeVel_;
+ uint8_t velStart : 7;
+ uint8_t velEnd : 7;
+ bool fadeRate_;
+ uint8_t rateStart_;
+ uint8_t rateEnd_;
+ uint8_t rateStartHz_;
+ uint8_t rateEndHz_;
+ };
+ // Saved variables
+ uint8_t chancePerc_ = 100;
+ uint8_t numOfRepeats_ : 6; // 1 to 64, stored as 0 - 63
+ uint8_t mode_ : 3; // Off, 1-Shot - Repeats for numOfRepeats_ restarts on new note on, On - Repeats indefinitely while key is hold, Hold - Endlessly repeats,
+ int8_t rateIndex_ : 5; // max 15 or -1 for hz
+ int8_t quantizedRateIndex_ : 5; // max 15 or -1 for hz
+ uint8_t rateHz_; // 0-255, gets remapped to a hertz float value
+ uint8_t gate_ : 8; // 0-200
+ bool fadeVel_; // Fade the velocity if true
+ uint8_t velStart_ : 7; // 0-127
+ uint8_t velEnd_ : 7; // 0-127
+ bool fadeRate_; // Fade the rate if true
+ uint8_t rateStart_ : 4; // 0-15
+ uint8_t rateEnd_ : 4; // 0-15
+ uint8_t rateStartHz_; // 0-255, gets remapped to a hertz float value
+ uint8_t rateEndHz_; // 0-255
+
+ // Consts
+ static const int queueSize = 16;
+
+ bool seqRunning_;
+
+ // Timing stuff
+ bool multiplierCalculated_ = false; // Used to prevent recalculating multiplier
+ float multiplier_ = 1;
+ uint8_t stepLength_ = 1; // length of note in arp steps
+
+ // Micros nextStepTimeP_ = 32;
+ // Micros lastStepTimeP_ = 32;
+ // uint32_t stepMicroDelta_ = 0;
+
+ // Micros last16thTime_ = 0;
+ // Micros next16thTime_ = 0;
+
+ // Calculated
+ bool quantizeSync_ = true;
+
+ float velStartPerc_ = 1.0f;
+ float velEndPerc_ = 1.0f;
+
+ float rateStartMult_ = 1;
+ float rateEndMult_ = 1;
+
+ // Rate in hertz
+ float rateInHz_;
+ float rateStartInHz_;
+ float rateEndInHz_;
+
+ MidiFXNoteMaster noteMaster;
+
+ static void processNoteForwarder(void *context, MidiNoteGroup *note)
+ {
+ static_cast(context)->processNoteInput(note);
+ }
+
+ static void sendNoteOutForwarder(void *context, MidiNoteGroup *note)
+ {
+ static_cast(context)->sendNoteOut(*note);
+ }
+
+ std::vector playedNoteQueue; // Keeps track of which notes are being played
+ std::vector activeNoteQueue; // Holds notes
+ std::vector pendingNoteQueue; // notes pending for quantization
+
+ std::vector tempNoteQueue; // Notes that are used in arp
+
+ std::vector fixedLengthNotes; // Tracking of fixed length notes
+
+ // MidiNoteGroup trackingNoteGroups[8];
+ // MidiNoteGroup trackingNoteGroupsPassthrough[8];
+
+ void trackNoteInputPassthrough(MidiNoteGroup *note, bool ignoreNoteOns);
+ void trackNoteInput(MidiNoteGroup *note);
+ void processNoteInput(MidiNoteGroup *note);
+
+ bool hasMidiNotes();
+ bool useRateHz();
+ void updateMultiplier();
+ bool insertMidiNoteQueue(MidiNoteGroup *note);
+ bool removeMidiNoteQueue(MidiNoteGroup *note);
+
+ static bool removeFromQueue(std::vector *queue, MidiNoteGroup *note);
+ static float rateToHz(uint8_t rateHz);
+
+ void changeRepeatMode(uint8_t newMode);
+ void repeatNoteOn(MidiNoteGroup *note);
+ void repeatNoteOff(MidiNoteGroup *note);
+ void startSeq();
+ void stopSeq();
+ void resetArpSeq();
+
+ void recalcVariables();
+
+ void triggerNote(RepeatNote note);
+ // void repeatNoteTrigger();
+ void playNote(uint32_t noteOnMicros, uint32_t lengthDelta, int16_t noteNumber, uint8_t velocity, uint8_t channel);
+ };
+}
diff --git a/Archive/OMX-27-firmware/src/midifx/midifx_scaler.cpp b/Archive/OMX-27-firmware/src/midifx/midifx_scaler.cpp
new file mode 100644
index 00000000..136b0a54
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/midifx/midifx_scaler.cpp
@@ -0,0 +1,359 @@
+#include "midifx_scaler.h"
+#include "../hardware/omx_disp.h"
+#include "../utils/music_scales.h"
+
+namespace midifx
+{
+ enum ScalerPage
+ {
+ SCLPAGE_1
+ };
+
+ MidiFXScaler::MidiFXScaler()
+ {
+ params_.addPage(4);
+ encoderSelect_ = true;
+ calculateRemap();
+ }
+
+ int MidiFXScaler::getFXType()
+ {
+ return MIDIFX_SCALER;
+ }
+
+ const char *MidiFXScaler::getName()
+ {
+ return "Scaler";
+ }
+
+ const char *MidiFXScaler::getDispName()
+ {
+ return "SCAL";
+ }
+
+ MidiFXInterface *MidiFXScaler::getClone()
+ {
+ auto clone = new MidiFXScaler();
+
+ clone->chancePerc_ = chancePerc_;
+ clone->useGlobalScale_ = useGlobalScale_;
+ clone->rootNote_ = rootNote_;
+ clone->scaleIndex_ = scaleIndex_;
+
+ clone->calculateRemap();
+
+ return clone;
+ }
+
+ void MidiFXScaler::onEnabled()
+ {
+ }
+
+ void MidiFXScaler::onDisabled()
+ {
+ }
+
+ void MidiFXScaler::noteInput(MidiNoteGroup note)
+ {
+ if (note.noteOff)
+ {
+ processNoteOff(note);
+ return;
+ }
+
+ // Probability that we scale the note
+ if (chancePerc_ != 100 && (chancePerc_ == 0 || random(100) > chancePerc_))
+ {
+ sendNoteOut(note);
+ return;
+ }
+
+ int8_t origNote = note.noteNumber;
+
+ // transpose original note by rootNote
+ // int8_t transposedNote = note.noteNumber + rootNote;
+
+ int8_t transposedNote = note.noteNumber;
+
+ int8_t noteIndex = transposedNote % 12;
+ int8_t octave = transposedNote / 12;
+
+ // offset for root note
+ // noteIndex = (noteIndex + rootNote) % 12;
+ // if(noteIndex + rootNote >= 12)
+ // {
+ // octave++;
+ // }
+
+ // noteIndex = (noteIndex + rootNote) % 12;
+
+ // noteIndex = (noteIndex - rootNote + 12) % 12;
+
+ int8_t remapedNoteIndex = scaleRemapper[noteIndex];
+
+ if (remapedNoteIndex > noteIndex)
+ {
+ octave--;
+ }
+
+ // remapedNoteIndex = (noteIndex + remapedNoteIndex) % 12;
+
+ // remove root note offset
+ // remapedNoteIndex = (remapedNoteIndex - rootNote + 12) % 12;
+
+ int8_t newNoteNumber = octave * 12 + remapedNoteIndex;
+
+ // untranspose
+ // newNoteNumber = newNoteNumber - rootNote;
+
+ // note out of range, kill
+ if (newNoteNumber < 0 || newNoteNumber > 127)
+ return;
+
+ note.noteNumber = newNoteNumber;
+
+ processNoteOn(origNote, note);
+
+ sendNoteOut(note);
+ }
+
+ // MidiNoteGroup MidiFXScaler::findTriggeredNote(uint8_t noteNumber)
+ // {
+ // for(int i = 0; i < triggeredNotes.size(); i++)
+ // {
+ // if(triggeredNotes[i].prevNoteNumber)
+
+ // }
+ // triggeredNotes.
+
+ // return nullptr;
+ // }
+
+ void MidiFXScaler::calculateRemap()
+ {
+ if (useGlobalScale_)
+ {
+ rootNote_ = scaleConfig.scaleRoot;
+ scaleIndex_ = scaleConfig.scalePattern;
+ }
+
+ if (scaleIndex_ < 0)
+ {
+ for (uint8_t i = 0; i < 12; i++)
+ {
+ scaleRemapper[i] = i; // Chromatic scale
+ }
+ return;
+ }
+
+ auto scalePattern = MusicScales::getScalePattern(scaleIndex_);
+
+ uint8_t scaleIndex = 0;
+ uint8_t lastNoteIndex = 0;
+
+ // looks through 12 notes, and sets each note to last note in scale
+ // so notes out of scale get rounded down to the previous note in the scale.
+ for (uint8_t i = 0; i < 12; i++)
+ {
+ if (scaleIndex < 7 && scalePattern[scaleIndex] == i)
+ {
+ lastNoteIndex = i;
+ scaleIndex++;
+ }
+
+ // uint8_t destIndex = (i - rootNote + 12) % 12;
+ // uint8_t destIndex = i + rootNote % 12;
+
+ scaleRemapper[i] = (lastNoteIndex + rootNote_) % 12;
+ }
+
+ if (rootNote_ > 0)
+ {
+ // rotate the scale to root
+ int8_t temp[12];
+
+ uint8_t val = 12 - rootNote_;
+
+ for (uint8_t i = 0; i < 12; i++)
+ {
+ temp[i] = scaleRemapper[(i + val) % 12];
+ }
+ for (int i = 0; i < 12; i++)
+ {
+ scaleRemapper[i] = temp[i];
+ }
+ }
+
+ // String msg = "scaleRemapper: ";
+
+ // for (int i = 0; i < 12; i++)
+ // {
+ // msg += String(scaleRemapper[i]) + ", ";
+ // }
+
+ // msg += "\n\n";
+
+ // Serial.println(msg);
+ }
+
+ void MidiFXScaler::loopUpdate()
+ {
+ if (useGlobalScale_)
+ {
+ int8_t prevRoot = rootNote_;
+ int8_t prevScale = scaleIndex_;
+
+ rootNote_ = scaleConfig.scaleRoot;
+ scaleIndex_ = scaleConfig.scalePattern;
+
+ if (rootNote_ != prevRoot || scaleIndex_ != prevScale)
+ {
+ calculateRemap();
+ }
+ }
+ }
+
+ void MidiFXScaler::onEncoderChangedEditParam(Encoder::Update enc)
+ {
+ int8_t page = params_.getSelPage();
+ int8_t param = params_.getSelParam();
+
+ auto amt = enc.accel(1);
+
+ if (page == SCLPAGE_1)
+ {
+ if (param == 0)
+ {
+ useGlobalScale_ = constrain(useGlobalScale_ + amt, 0, 1);
+ if (amt != 0)
+ {
+ if (useGlobalScale_)
+ {
+ omxDisp.displayMessage("Global: ON");
+ }
+ else
+ {
+ omxDisp.displayMessage("Global: OFF");
+ }
+ calculateRemap();
+ }
+ }
+ else if (param == 1)
+ {
+ if (useGlobalScale_)
+ {
+ int prevRoot = scaleConfig.scaleRoot;
+ scaleConfig.scaleRoot = constrain(scaleConfig.scaleRoot + amt, 0, 12 - 1);
+ if (prevRoot != scaleConfig.scaleRoot)
+ {
+ calculateRemap();
+ }
+ }
+ else
+ {
+ int prevRoot = rootNote_;
+ rootNote_ = constrain(rootNote_ + amt, 0, 12 - 1);
+ if (prevRoot != rootNote_)
+ {
+ calculateRemap();
+ }
+ }
+ }
+ else if (param == 2)
+ {
+ if (useGlobalScale_)
+ {
+ int prevPat = scaleConfig.scalePattern;
+ scaleConfig.scalePattern = constrain(scaleConfig.scalePattern + amt, -1, MusicScales::getNumScales() - 1);
+ if (prevPat != scaleConfig.scalePattern)
+ {
+ omxDisp.displayMessage(MusicScales::getScaleName(scaleConfig.scalePattern));
+ calculateRemap();
+ }
+ }
+ else
+ {
+ int prevPat = scaleIndex_;
+ scaleIndex_ = constrain(scaleIndex_ + amt, -1, MusicScales::getNumScales() - 1);
+ if (prevPat != scaleIndex_)
+ {
+ omxDisp.displayMessage(MusicScales::getScaleName(scaleIndex_));
+ calculateRemap();
+ }
+ }
+ }
+ else if (param == 3)
+ {
+ chancePerc_ = constrain(chancePerc_ + amt, 0, 100);
+ }
+ }
+
+ omxDisp.setDirty();
+ }
+
+ void MidiFXScaler::onDisplayUpdate(uint8_t funcKeyMode)
+ {
+ omxDisp.clearLegends();
+
+ int8_t page = params_.getSelPage();
+
+ switch (page)
+ {
+ case SCLPAGE_1:
+ {
+ omxDisp.legends[0] = "GLBL";
+ omxDisp.legendText[0] = useGlobalScale_ ? "ON" : "OFF";
+
+ omxDisp.legends[1] = "ROOT";
+ omxDisp.legendVals[1] = -127;
+ omxDisp.legendText[1] = MusicScales::getNoteName(rootNote_);
+
+ omxDisp.legends[2] = "SCALE";
+ if (scaleIndex_ < 0)
+ {
+ omxDisp.legendVals[2] = -127;
+ omxDisp.legendText[2] = "Off";
+ }
+ else
+ {
+ omxDisp.legendVals[2] = scaleIndex_;
+ }
+
+ omxDisp.legends[3] = "CHC%";
+ omxDisp.legendVals[3] = -127;
+ omxDisp.useLegendString[3] = true;
+ omxDisp.legendString[3] = String(chancePerc_) + "%";
+ }
+ break;
+ default:
+ break;
+ }
+
+ omxDisp.dispGenericMode2(params_.getNumPages(), params_.getSelPage(), params_.getSelParam(), getEncoderSelect());
+ }
+
+ int MidiFXScaler::saveToDisk(int startingAddress, Storage *storage)
+ {
+ // Serial.println((String) "Saving mfx scaler: " + startingAddress); // 5969
+ storage->write(startingAddress + 0, chancePerc_);
+ storage->write(startingAddress + 1, useGlobalScale_);
+ storage->write(startingAddress + 2, (uint8_t)rootNote_);
+ storage->write(startingAddress + 3, (uint8_t)scaleIndex_);
+
+ return startingAddress + 4;
+ }
+
+ int MidiFXScaler::loadFromDisk(int startingAddress, Storage *storage)
+ {
+ // Serial.println((String) "Loading mfx scaler: " + startingAddress); // 5969
+
+ chancePerc_ = storage->read(startingAddress + 0);
+ useGlobalScale_ = (bool)storage->read(startingAddress + 1);
+ rootNote_ = (int8_t)storage->read(startingAddress + 2);
+ scaleIndex_ = (int8_t)storage->read(startingAddress + 3);
+
+ calculateRemap();
+
+ return startingAddress + 4;
+ }
+}
diff --git a/Archive/OMX-27-firmware/src/midifx/midifx_scaler.h b/Archive/OMX-27-firmware/src/midifx/midifx_scaler.h
new file mode 100644
index 00000000..6daeb3d7
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/midifx/midifx_scaler.h
@@ -0,0 +1,51 @@
+#pragma once
+
+#include "midifx_interface.h"
+
+namespace midifx
+{
+
+ class MidiFXScaler : public MidiFXInterface
+ {
+ public:
+ MidiFXScaler();
+ ~MidiFXScaler() {}
+
+ int getFXType() override;
+ const char *getName() override;
+ const char *getDispName() override;
+
+ MidiFXInterface *getClone() override;
+
+ void loopUpdate() override;
+
+ void onDisplayUpdate(uint8_t funcKeyMode) override;
+
+ void noteInput(MidiNoteGroup note) override;
+
+ int saveToDisk(int startingAddress, Storage *storage) override;
+ int loadFromDisk(int startingAddress, Storage *storage) override;
+
+ protected:
+ void onEnabled() override;
+ void onDisabled() override;
+
+ void onEncoderChangedEditParam(Encoder::Update enc) override;
+
+ private:
+ // std::vector triggeredNotes;
+
+ uint8_t chancePerc_ = 100;
+
+ bool useGlobalScale_ = true;
+
+ int8_t rootNote_ = 0;
+ int8_t scaleIndex_ = 0;
+
+ int8_t scaleRemapper[12];
+
+ void calculateRemap();
+
+ // MidiNoteGroup findTriggeredNote(uint8_t noteNumber);
+ };
+}
diff --git a/Archive/OMX-27-firmware/src/midifx/midifx_selector.cpp b/Archive/OMX-27-firmware/src/midifx/midifx_selector.cpp
new file mode 100644
index 00000000..1a0125c0
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/midifx/midifx_selector.cpp
@@ -0,0 +1,268 @@
+#include "midifx_selector.h"
+#include "../hardware/omx_disp.h"
+
+namespace midifx
+{
+ enum MidiFXSelectorModes
+ {
+ MFXSELMODE_RAND,
+ MFXSELMODE_UP,
+ MFXSELMODE_DOWN,
+ MFXSELMODE_COUNT
+ };
+
+ const char* modeLabels[3] = {"RND", "UP", "DOWN"};
+
+
+ MidiFXSelector::MidiFXSelector()
+ {
+ params_.addPage(4);
+
+ mode_ = MFXSELMODE_RAND;
+ length_ = 2;
+ chancePerc_ = 100;
+
+ encoderSelect_ = true;
+ }
+
+ int MidiFXSelector::getFXType()
+ {
+ return MIDIFX_SELECTOR;
+ }
+
+ const char *MidiFXSelector::getName()
+ {
+ return "Selector";
+ }
+
+ const char *MidiFXSelector::getDispName()
+ {
+ return "SEL";
+ }
+
+ MidiFXInterface *MidiFXSelector::getClone()
+ {
+ auto clone = new MidiFXSelector();
+
+ clone->chancePerc_ = chancePerc_;
+ clone->mode_ = mode_;
+ clone->length_ = length_;
+
+ return clone;
+ }
+
+ void MidiFXSelector::onEnabled()
+ {
+ }
+
+ void MidiFXSelector::onDisabled()
+ {
+ }
+
+ void MidiFXSelector::handleNoteOff(MidiNoteGroup note)
+ {
+ processNoteOff(note);
+ }
+
+ bool MidiFXSelector::chanceShouldSkip()
+ {
+ return (chancePerc_ != 100 && (chancePerc_ == 0 || random(100) > chancePerc_));
+ }
+
+ uint8_t MidiFXSelector::getLength()
+ {
+ return length_;
+ }
+
+ bool MidiFXSelector::didLengthChange()
+ {
+ bool changed = lengthChanged_;
+ lengthChanged_ = false;
+ return changed;
+ }
+
+ uint8_t MidiFXSelector::getFinalMidiFXIndex(uint8_t thisMFXIndex)
+ {
+ return thisMFXIndex + length_ + 1; // +1 to account for this index, mfxIndex: 0 with length 2 should return index 3
+ }
+
+ uint8_t MidiFXSelector::getSelectedMidiFXIndex(uint8_t thisMFXIndex)
+ {
+ if(length_ == 1)
+ {
+ return thisMFXIndex + 1;
+ }
+
+ if(length_ == 0)
+ {
+ return -1;
+ }
+
+ switch (mode_)
+ {
+ case MFXSELMODE_RAND:
+ {
+ return thisMFXIndex + random(1, length_ + 1);
+ }
+ case MFXSELMODE_UP:
+ {
+ uint8_t selIndex = thisMFXIndex + selPos_ + 1;
+ selPos_ = (selPos_ + length_ + 1) % length_;
+ return selIndex;
+ }
+ case MFXSELMODE_DOWN:
+ {
+ uint8_t selIndex = thisMFXIndex + selPos_ + 1;
+ selPos_ = (selPos_ + length_ - 1) % length_;
+ return selIndex;
+ }
+ default:
+ break;
+ }
+
+ return thisMFXIndex + 1;
+ }
+
+ void MidiFXSelector::setNoteInputFunc(uint8_t slotIndex, void (*fptr)(void *, midifx::MidiFXSelector *, uint8_t, MidiNoteGroup), void *context)
+ {
+ setSlotIndex(slotIndex);
+ noteInputContext_ = context;
+ noteInputFunctionPtr_ = fptr;
+ }
+
+ void MidiFXSelector::noteInput(MidiNoteGroup note)
+ {
+ // NoteInput handled by function on MidiFX group
+ if (noteInputContext_ != nullptr)
+ {
+ noteInputFunctionPtr_(noteInputContext_, this, mfxSlotIndex_, note);
+ return;
+ }
+
+ sendNoteOut(note);
+
+
+ // if (note.noteOff)
+ // {
+ // processNoteOff(note);
+ // return;
+ // }
+
+ // if (chancePerc_ != 100 && (chancePerc_ == 0 || random(100) > chancePerc_))
+ // {
+ // sendNoteOut(note);
+ // return;
+ // }
+
+ // this->setNoteOutput()
+
+
+
+
+
+ // int8_t origNote = note.noteNumber;
+
+ // int newNoteNumber = origNote + transpose_ + (octave_ * 12);
+
+ // if (newNoteNumber >= 0 && newNoteNumber <= 127)
+ // {
+ // note.noteNumber = newNoteNumber;
+ // sendNoteOut(note);
+ // }
+ }
+
+ void MidiFXSelector::loopUpdate()
+ {
+ }
+
+ void MidiFXSelector::onEncoderChangedEditParam(Encoder::Update enc)
+ {
+ int8_t page = params_.getSelPage();
+ int8_t param = params_.getSelParam();
+
+ auto amt = enc.accel(1);
+
+ if (page == 0)
+ {
+ if (param == 0)
+ {
+ mode_ = constrain(mode_ + amt, 0, MFXSELMODE_COUNT - 1);
+ }
+ else if (param == 1)
+ {
+ uint8_t oldLength = length_;
+ length_ = constrain(length_ + amt, 0, 7);
+ lengthChanged_ = oldLength != length_;
+ }
+ else if (param == 3)
+ {
+ chancePerc_ = constrain(chancePerc_ + amt, 0, 100);
+ }
+ }
+
+ omxDisp.setDirty();
+ }
+
+ void MidiFXSelector::onDisplayUpdate(uint8_t funcKeyMode)
+ {
+ omxDisp.clearLegends();
+
+ int8_t page = params_.getSelPage();
+
+ switch (page)
+ {
+ case 0:
+ {
+ omxDisp.legends[0] = "MODE";
+ omxDisp.legends[1] = "LEN";
+ omxDisp.legends[3] = "CHC%";
+ omxDisp.legendText[0] = modeLabels[mode_];
+ omxDisp.legendVals[1] = length_;
+ omxDisp.useLegendString[3] = true;
+ omxDisp.legendString[3] = String(chancePerc_) + "%";
+ }
+ break;
+ default:
+ break;
+ }
+
+ omxDisp.dispGenericMode2(params_.getNumPages(), params_.getSelPage(), params_.getSelParam(), getEncoderSelect());
+ }
+
+ int MidiFXSelector::saveToDisk(int startingAddress, Storage *storage)
+ {
+ SelectorSave save;
+ save.chancePerc = chancePerc_;
+ save.mode = mode_;
+ save.length = length_;
+
+ int saveSize = sizeof(SelectorSave);
+
+ auto saveBytesPtr = (byte *)(&save);
+ for (int j = 0; j < saveSize; j++)
+ {
+ storage->write(startingAddress + j, *saveBytesPtr++);
+ }
+
+ return startingAddress + saveSize;
+ }
+
+ int MidiFXSelector::loadFromDisk(int startingAddress, Storage *storage)
+ {
+ int saveSize = sizeof(SelectorSave);
+
+ auto save = SelectorSave{};
+ auto current = (byte *)&save;
+ for (int j = 0; j < saveSize; j++)
+ {
+ *current = storage->read(startingAddress + j);
+ current++;
+ }
+
+ chancePerc_ = save.chancePerc;
+ mode_ = save.mode;
+ length_ = save.length;
+
+ return startingAddress + saveSize;
+ }
+}
diff --git a/Archive/OMX-27-firmware/src/midifx/midifx_selector.h b/Archive/OMX-27-firmware/src/midifx/midifx_selector.h
new file mode 100644
index 00000000..20446e69
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/midifx/midifx_selector.h
@@ -0,0 +1,81 @@
+#pragma once
+
+#include "midifx_interface.h"
+
+namespace midifx
+{
+
+ class MidiFXSelector : public MidiFXInterface
+ {
+ public:
+ MidiFXSelector();
+ ~MidiFXSelector() {}
+
+ int getFXType() override;
+ const char *getName() override;
+ const char *getDispName() override;
+
+ MidiFXInterface *getClone() override;
+
+ void loopUpdate() override;
+
+ void onDisplayUpdate(uint8_t funcKeyMode) override;
+
+ void noteInput(MidiNoteGroup note) override;
+
+ int saveToDisk(int startingAddress, Storage *storage) override;
+ int loadFromDisk(int startingAddress, Storage *storage) override;
+
+ void handleNoteOff(MidiNoteGroup note);
+
+ bool chanceShouldSkip();
+
+ uint8_t getLength();
+
+ bool didLengthChange();
+
+ // The next mfx index after going through the selector
+ // If selector is in slot 1, length is 2, then the next mfx slot index
+ // would be slot 4
+ // if the index returned is greater than hte number of mfx slots(8)
+ // then it will go to the group's output.
+ uint8_t getFinalMidiFXIndex(uint8_t thisMFXIndex);
+
+ // This will return which MidiFX Index to go to
+ // If selector is in slot 1, has a length of 2
+ // then the note can go to either slot 2 or slot 3.
+ // If the is no mfx at slot 2 or slot 3, the note will go to
+ // slot 4.
+ // If there is no midifx at slot 4, it will go to next slot with midifx
+ // or the midifx group output if there are none
+ uint8_t getSelectedMidiFXIndex(uint8_t thisMFXIndex);
+
+ void setNoteInputFunc(uint8_t slotIndex, void (*fptr)(void *, midifx::MidiFXSelector *, uint8_t, MidiNoteGroup), void *context);
+
+ protected:
+ void onEnabled() override;
+ void onDisabled() override;
+
+ void onEncoderChangedEditParam(Encoder::Update enc) override;
+
+ private:
+ struct SelectorSave
+ {
+ uint8_t mode : 2;
+ uint8_t length : 3; // 2-7
+ uint8_t chancePerc : 7;
+ };
+
+ uint8_t mode_ : 2;
+ uint8_t length_ : 3; // 2-7
+
+ uint8_t chancePerc_ = 100;
+
+ uint8_t selPos_ = 0;
+
+ bool lengthChanged_;
+
+ void *noteInputContext_;
+ void (*noteInputFunctionPtr_)(void *, midifx::MidiFXSelector *, uint8_t, MidiNoteGroup);
+ };
+}
diff --git a/Archive/OMX-27-firmware/src/midifx/midifx_transpose.cpp b/Archive/OMX-27-firmware/src/midifx/midifx_transpose.cpp
new file mode 100644
index 00000000..3cfeff30
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/midifx/midifx_transpose.cpp
@@ -0,0 +1,172 @@
+#include "midifx_transpose.h"
+#include "../hardware/omx_disp.h"
+
+namespace midifx
+{
+ MidiFXTranspose::MidiFXTranspose()
+ {
+ params_.addPage(4);
+
+ octave_ = 0;
+ transpose_ = 0;
+
+ encoderSelect_ = true;
+ }
+
+ int MidiFXTranspose::getFXType()
+ {
+ return MIDIFX_TRANSPOSE;
+ }
+
+ const char *MidiFXTranspose::getName()
+ {
+ return "Transpose";
+ }
+
+ const char *MidiFXTranspose::getDispName()
+ {
+ return "TRAN";
+ }
+
+ MidiFXInterface *MidiFXTranspose::getClone()
+ {
+ auto clone = new MidiFXTranspose();
+
+ clone->chancePerc_ = chancePerc_;
+ clone->transpose_ = transpose_;
+ clone->octave_ = octave_;
+
+ return clone;
+ }
+
+ void MidiFXTranspose::onEnabled()
+ {
+ }
+
+ void MidiFXTranspose::onDisabled()
+ {
+ }
+
+ void MidiFXTranspose::noteInput(MidiNoteGroup note)
+ {
+ if (note.noteOff)
+ {
+ processNoteOff(note);
+ return;
+ }
+
+ if (chancePerc_ != 100 && (chancePerc_ == 0 || random(100) > chancePerc_))
+ {
+ sendNoteOut(note);
+ return;
+ }
+
+ int8_t origNote = note.noteNumber;
+
+ int newNoteNumber = origNote + transpose_ + (octave_ * 12);
+
+ if (newNoteNumber >= 0 && newNoteNumber <= 127)
+ {
+ note.noteNumber = newNoteNumber;
+ sendNoteOut(note);
+ }
+ }
+
+ void MidiFXTranspose::loopUpdate()
+ {
+ }
+
+ void MidiFXTranspose::onEncoderChangedEditParam(Encoder::Update enc)
+ {
+ int8_t page = params_.getSelPage();
+ int8_t param = params_.getSelParam();
+
+ auto amt = enc.accel(1);
+
+ if (page == 0)
+ {
+ if (param == 0)
+ {
+ transpose_ = constrain(transpose_ + amt, -24, 24);
+ }
+ else if (param == 1)
+ {
+ octave_ = constrain(octave_ + amt, -6, 6);
+ }
+ else if (param == 3)
+ {
+ chancePerc_ = constrain(chancePerc_ + amt, 0, 100);
+ }
+ }
+
+ omxDisp.setDirty();
+ }
+
+ void MidiFXTranspose::onDisplayUpdate(uint8_t funcKeyMode)
+ {
+ omxDisp.clearLegends();
+
+ int8_t page = params_.getSelPage();
+
+ // uint8_t starti = 0;
+ // uint8_t endi = 0;
+
+ switch (page)
+ {
+ case 0:
+ {
+ omxDisp.legends[0] = "ST";
+ omxDisp.legends[1] = "OCT";
+ omxDisp.legends[3] = "CHC%";
+ omxDisp.useLegendString[0] = true;
+ omxDisp.useLegendString[1] = true;
+ omxDisp.useLegendString[3] = true;
+ omxDisp.legendString[0] = transpose_ == 0 ? "-" : (transpose_ >= 0 ? ("+" + String(transpose_)) : (String(transpose_)));
+ omxDisp.legendString[1] = octave_ == 0 ? "-" : (octave_ >= 0 ? ("+" + String(octave_)) : (String(octave_)));
+ omxDisp.legendString[3] = String(chancePerc_) + "%";
+ }
+ break;
+ default:
+ break;
+ }
+
+ omxDisp.dispGenericMode2(params_.getNumPages(), params_.getSelPage(), params_.getSelParam(), getEncoderSelect());
+ }
+
+ int MidiFXTranspose::saveToDisk(int startingAddress, Storage *storage)
+ {
+ TransposeSave save;
+ save.chancePerc_ = chancePerc_;
+ save.transpose = transpose_;
+ save.octave = octave_;
+
+ int saveSize = sizeof(TransposeSave);
+
+ auto saveBytesPtr = (byte *)(&save);
+ for (int j = 0; j < saveSize; j++)
+ {
+ storage->write(startingAddress + j, *saveBytesPtr++);
+ }
+
+ return startingAddress + saveSize;
+ }
+
+ int MidiFXTranspose::loadFromDisk(int startingAddress, Storage *storage)
+ {
+ int saveSize = sizeof(TransposeSave);
+
+ auto save = TransposeSave{};
+ auto current = (byte *)&save;
+ for (int j = 0; j < saveSize; j++)
+ {
+ *current = storage->read(startingAddress + j);
+ current++;
+ }
+
+ transpose_ = save.transpose;
+ octave_ = save.octave;
+ chancePerc_ = save.chancePerc_;
+
+ return startingAddress + saveSize;
+ }
+}
diff --git a/Archive/OMX-27-firmware/src/midifx/midifx_transpose.h b/Archive/OMX-27-firmware/src/midifx/midifx_transpose.h
new file mode 100644
index 00000000..9577a0a5
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/midifx/midifx_transpose.h
@@ -0,0 +1,48 @@
+#pragma once
+
+#include "midifx_interface.h"
+
+namespace midifx
+{
+
+ class MidiFXTranspose : public MidiFXInterface
+ {
+ public:
+ MidiFXTranspose();
+ ~MidiFXTranspose() {}
+
+ int getFXType() override;
+ const char *getName() override;
+ const char *getDispName() override;
+
+ MidiFXInterface *getClone() override;
+
+ void loopUpdate() override;
+
+ void onDisplayUpdate(uint8_t funcKeyMode) override;
+
+ void noteInput(MidiNoteGroup note) override;
+
+ int saveToDisk(int startingAddress, Storage *storage) override;
+ int loadFromDisk(int startingAddress, Storage *storage) override;
+
+ protected:
+ void onEnabled() override;
+ void onDisabled() override;
+
+ void onEncoderChangedEditParam(Encoder::Update enc) override;
+
+ private:
+ struct TransposeSave
+ {
+ int8_t transpose : 6;
+ int8_t octave : 4;
+ uint8_t chancePerc_ = 100;
+ };
+
+ int8_t transpose_ : 6;
+ int8_t octave_ : 4;
+
+ uint8_t chancePerc_ = 100;
+ };
+}
diff --git a/Archive/OMX-27-firmware/src/midimacro/midimacro_deluge.cpp b/Archive/OMX-27-firmware/src/midimacro/midimacro_deluge.cpp
new file mode 100644
index 00000000..166bfd6b
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/midimacro/midimacro_deluge.cpp
@@ -0,0 +1,714 @@
+#include "midimacro_deluge.h"
+#include "../utils/omx_util.h"
+#include "../hardware/omx_disp.h"
+#include "../hardware/omx_leds.h"
+#include "../midi/midi.h"
+#include "../consts/consts.h"
+#include "../consts/colors.h"
+
+namespace midimacro
+{
+ // uint8_t delCCBanks[4][5] = {
+ // {74, 71, 70, 0, 0},
+ // {0, 0, 0, 0, 0},
+ // {0, 0, 0, 0, 0},
+ // {0, 0, 0, 0, 0},
+ // };
+
+ // const char *delParams[4][5] = {
+ // {"FREQ", "RES", "MORP", "", ""},
+ // {"", "", "", "", ""},
+ // {"", "", "", "", ""},
+ // {"", "", "", "", ""},
+ // };
+
+ // const char *pageNames[4] = {"Filt 1", "Filt 2", "Env 1", "Env 2"};
+
+ enum DelugePage
+ {
+ DELPAGE_FILT1,
+ DELPAGE_FILT2,
+ DELPAGE_ENV1,
+ DELPAGE_ENV2
+ };
+
+ MidiMacroDeluge::MidiMacroDeluge()
+ {
+ // Top Row Banks
+ paramBanks[0].bankName = "Env 1";
+ paramBanks[0].keyMap = 1;
+ paramBanks[0].keyColor = BLUE;
+ paramBanks[0].SetCCs("Attack", 73, "Decay", 75, "Sustain", 76, "Release", 72);
+
+ paramBanks[1].bankName = "Env 2";
+ paramBanks[1].keyMap = 2;
+ paramBanks[1].keyColor = BLUE;
+ paramBanks[1].SetCCs("Attack", 77, "Decay", 78, "Sustain", 79, "Release", 80);
+
+
+ paramBanks[2].bankName = "LPF";
+ paramBanks[2].keyMap = 3;
+ paramBanks[2].keyColor = RED;
+ paramBanks[2].SetCCs("Freq", 74, "Res", 71, "Morph", 70);
+
+ paramBanks[3].bankName = "HPF";
+ paramBanks[3].keyMap = 4;
+ paramBanks[3].keyColor = RED;
+ paramBanks[3].SetCCs("Freq", 81, "Res", 82, "Morph", 83);
+
+ paramBanks[4].bankName = "EQ";
+ paramBanks[4].keyMap = 5;
+ paramBanks[4].keyColor = RED;
+ paramBanks[4].SetCCs("Bas Freq", 84, "Bass LVL", 86, "Treb Freq", 85, "Treb LVL", 87);
+
+ // Bot Row Banks
+ paramBanks[5].bankName = "Master";
+ paramBanks[5].keyMap = 11;
+ paramBanks[5].keyColor = GREEN;
+ paramBanks[5].SetCCs("Pan", 10, "Transp", 3, "Porta", 5, "", 255, "Level", 7);
+
+ // OSC1 and FM1 Mapped to same key with toggle
+ paramBanks[6].bankName = "OSC 1";
+ paramBanks[6].keyMap = 12;
+ paramBanks[6].altBank = false;
+ paramBanks[6].keyColor = ROSE;
+ paramBanks[6].SetCCs("Level", 21, "Transp", 12, "PW", 23, "FM Fdbk", 24, "WT Morph", 25);
+
+ paramBanks[7].bankName = "FM 1";
+ paramBanks[7].keyMap = 12;
+ paramBanks[7].altBank = true;
+ paramBanks[7].keyColor = ROSE;
+ paramBanks[7].SetCCs("Level", 54, "Transp", 14, "Feedback", 55);
+
+ // OSC2 and FM2 Mapped to same key with toggle
+ paramBanks[8].bankName = "OSC 2";
+ paramBanks[8].keyMap = 13;
+ paramBanks[8].altBank = false;
+ paramBanks[8].keyColor = ROSE;
+ paramBanks[8].SetCCs("Level", 26, "Transp", 13, "PW", 28, "FM Fdbk", 29, "WT Morph", 30);
+
+ paramBanks[9].bankName = "FM 2";
+ paramBanks[9].keyMap = 13;
+ paramBanks[9].altBank = true;
+ paramBanks[9].keyColor = ROSE;
+ paramBanks[9].SetCCs("Level", 56, "Transp", 15, "Feedback", 57);
+
+ paramBanks[10].bankName = "LFO Delay Reverb";
+ paramBanks[10].keyMap = 14;
+ paramBanks[10].keyColor = ORANGE;
+ paramBanks[10].SetCCs("LFO1 Rate", 58, "LFO2 Rate", 59, "DEL Rate", 53, "Delay", 52, "Reverb", 91);
+
+ paramBanks[11].bankName = "ModFX";
+ paramBanks[11].keyMap = 14;
+ paramBanks[11].altBank = true;
+ paramBanks[11].keyColor = MAGENTA;
+ paramBanks[11].SetCCs("Rate", 16, "Depth", 93, "Feedback", 17, "Offset", 18);
+
+ paramBanks[12].bankName = "Distortion Noise";
+ paramBanks[12].keyMap = 15;
+ paramBanks[12].keyColor = RED;
+ paramBanks[12].SetCCs("Bitcrush", 62, "Decimate", 63, "Wavefold", 19, "Noise", 41);
+
+ paramBanks[13].bankName = "Arp Sidechain";
+ paramBanks[13].keyMap = 16;
+ paramBanks[13].keyColor = LIME;
+ paramBanks[13].SetCCs("Arp Rate", 51, "Arp Gate", 50, "Vol Duck", 61, "SC Shape", 60);
+
+ paramBanks[14].bankName = "Custom 1";
+ paramBanks[14].keyMap = 17;
+ paramBanks[14].keyColor = ORANGE;
+ paramBanks[14].SetCCs("Pot 1", 100, "Pot 2", 101, "Pot 3", 102, "Pot 4", 103, "Pot 5", 104);
+
+ paramBanks[15].bankName = "Custom 2";
+ paramBanks[15].keyMap = 17;
+ paramBanks[15].altBank = true;
+ paramBanks[15].keyColor = ORANGE;
+ paramBanks[15].SetCCs("Pot 1", 105, "Pot 2", 106, "Pot 3", 107, "Pot 4", 108, "Pot 5", 109);
+
+
+ params_.addPage(5); //
+ // params_.addPage(1); //
+ // params_.addPage(1); //
+ // params_.addPage(1); //
+
+ for(uint8_t i = 0; i < 127; i++)
+ {
+ delVals[i] = 0;
+ }
+
+ encoderSelect_ = true;
+ }
+
+ String MidiMacroDeluge::getName()
+ {
+ return String("DELUGE");
+ }
+
+ MidiParamBank *MidiMacroDeluge::getActiveBank()
+ {
+ // int8_t selPage = params_.getSelPage();
+
+ // if (selPage < 0 || selPage >= 4)
+ // {
+ // return nullptr;
+ // }
+
+ return ¶mBanks[selBank];
+ }
+
+ void MidiMacroDeluge::keyDownBankShortcut(uint8_t keyIndex)
+ {
+ auto activeBank = getActiveBank();
+
+ bool selAltBank = false;
+
+ if(activeBank->keyMap == keyIndex)
+ {
+ // If the active bank's keyMap matches this key then we have opprotunity to select an alt bank if one exists
+ // If the active bank is an altbank, then the main level bank will be selected
+ // TLDR: Pressing a key multiple times can toggle between different banks
+ selAltBank = activeBank->altBank == false;
+ }
+
+ for(uint8_t i = 0; i < kNumBanks; i++)
+ {
+ if(paramBanks[i].keyMap == keyIndex && paramBanks[i].altBank == selAltBank)
+ {
+ setActiveBank(i);
+ return;
+ }
+ }
+ }
+
+ void MidiMacroDeluge::setActiveBank(uint8_t bankIndex)
+ {
+ if (bankIndex >= kNumBanks)
+ {
+ Serial.println((String)"ERROR:MidiMacroDeluge: Cannot set active bank to " + bankIndex);
+ return;
+ }
+
+ if (bankIndex != selBank)
+ {
+ // save/load activeParam to bank
+ paramBanks[selBank].activeParam = activeParam;
+ selBank = bankIndex;
+ activeParam = paramBanks[selBank].activeParam;
+
+ if(params_.getSelPage() == 0)
+ {
+ params_.setSelParam(activeParam);
+ }
+
+ updatePotPickups();
+ omxDisp.setDirty();
+ omxLeds.setDirty();
+ }
+ }
+
+ // Updates the pot pickups to the values saved in the active bank
+ // Thus if we switch banks, the value will need to be picked up
+ // by the pot before it sends out to avoid jumping values.
+ void MidiMacroDeluge::updatePotPickups()
+ {
+ auto activeBank = getActiveBank();
+
+ // Update the potPickups to the values of the active bank
+ if(activeBank != nullptr)
+ {
+ for(int8_t i = 0; i < 5; i++)
+ {
+ potPickups[i].SetVal(activeBank->midiValues[i], false);
+ potPickups[i].SaveRevertVal();
+ }
+ }
+ }
+
+ // Reverts the values of current bank and sends update via midiCC
+ // Revert value is saved when switching banks or when a new value comes in through midi
+ void MidiMacroDeluge::revertPotPickups()
+ {
+ auto activeBank = getActiveBank();
+
+ for(int8_t i = 0; i < 5; i++)
+ {
+ if(activeBank->midiCCs[i] < 128)
+ {
+ potPickups[i].RevertVal();
+ MM::sendControlChange(activeBank->midiCCs[i], potPickups[i].value, midiMacroConfig.midiMacroChan);
+ }
+ }
+ }
+
+ void MidiMacroDeluge::onEnabled()
+ {
+ omxDisp.displayMessage("Deluge");
+
+ auxDown_ = false;
+
+ updatePotPickups();
+ }
+
+ void MidiMacroDeluge::onDisabled()
+ {
+ }
+
+ void MidiMacroDeluge::inMidiControlChange(byte channel, byte control, byte value)
+ {
+ if(channel == midiMacroConfig.midiMacroChan)
+ {
+ // delVals[control] = value; // Might want to do this for speed
+
+ // auto activeBank = getActiveBank();
+
+ // if(activeBank != nullptr)
+ // {
+ // // activeBank->UpdateCCValue(control, value);
+
+ // int8_t paramIndex = activeBank->UpdateCCValue(control, value);
+
+ // // CC was found in bank and this is the active bank
+ // if (paramIndex >= 0)
+ // {
+ // // Update the pot pickup for this index.
+ // potPickups[paramIndex].SetVal(value);
+ // // omxDisp.displayMessageTimed("CC " + String(control) + " Val " + String(value), 5);
+ // }
+ // }
+
+ for (uint8_t i = 0; i < kNumBanks; i++)
+ {
+ int8_t paramIndex = paramBanks[i].UpdateCCValue(control, value);
+
+ // CC was found in bank and this is the active bank
+ if (paramIndex >= 0 && i == selBank)
+ {
+ // Update the pot pickup for this index.
+ potPickups[paramIndex].SetVal(value, true);
+ omxDisp.setDirty();
+ // omxDisp.displayMessageTimed("CC " + String(control) + " Val " + String(value), 5);
+ }
+ }
+
+ // int8_t selPage = params_.getSelPage();
+
+ // auto activeBank = getActiveBank();
+
+ // if(activeBank != nullptr)
+ // {
+
+ // }
+
+ // Serial.println((String)"IN CC: " + control + " VAL: " + value); // 5968
+ }
+ }
+
+ void MidiMacroDeluge::loopUpdate()
+ {
+ }
+
+ void MidiMacroDeluge::onPotChanged(int potIndex, int prevValue, int newValue, int analogDelta)
+ {
+ // omxDisp.displayMessageTimed("Pot Index " + String(potIndex), 5);
+
+ auto activeBank = getActiveBank();
+
+ if (activeBank != nullptr)
+ {
+ uint8_t cc = activeBank->midiCCs[potIndex];
+
+ if (cc <= 127 && newValue != prevValue)
+ {
+ potPickups[potIndex].UpdatePot(prevValue, newValue);
+ activeBank->UpdatePotValue(potIndex, potPickups[potIndex].value);
+
+ uint8_t oldValDel = (uint8_t)map(prevValue, 0, 127, 0, 50);
+ uint8_t newValDel = (uint8_t)map(newValue, 0, 127, 0, 50);
+
+ if (newValDel != oldValDel)
+ {
+ activeParam = potIndex;
+ }
+
+ if(potPickups[potIndex].pickedUp)
+ {
+ // omxDisp.displayMessageTimed(String(activeBank->paramNames[potIndex]) + " " + String(cc) + " " + String(potPickups[potIndex].value), 5);
+
+ // uint8_t delugeMapVal = (uint8_t)map(potPickups[potIndex].value, 0, 127, 0, 50);
+
+ // omxDisp.displayMessageTimed(String(activeBank->paramNames[potIndex]) + " " + String(delugeMapVal), 5);
+ MM::sendControlChange(cc, potPickups[potIndex].value, midiMacroConfig.midiMacroChan);
+ }
+ else
+ {
+ // uint8_t delugeMapNewVal = (uint8_t)map(newValue, 0, 127, 0, 50);
+ // uint8_t delugeMapVal = (uint8_t)map(potPickups[potIndex].value, 0, 127, 0, 50);
+ // omxDisp.displayMessageTimed(String(delugeMapNewVal) + " -> " + String(delugeMapVal), 5);
+ }
+ }
+ }
+
+ // uint8_t oldV = manStrumSensit_;
+ // manStrumSensit_ = (uint8_t)map(newValue, 0, 127, 1, 32);
+ // if (manStrumSensit_ != oldV)
+ // {
+ // omxDisp.displayMessageTimed("Sens: " + String(manStrumSensit_), 5);
+ // }
+
+ // omxUtil.sendPots(potIndex, midiMacroConfig.midiMacroChan);
+ }
+
+ void MidiMacroDeluge::onKeyUpdate(OMXKeypadEvent e)
+ {
+ uint8_t thisKey = e.key();
+ // int keyPos = thisKey - 11;
+
+ // AUX KEY
+ if (thisKey == 0)
+ {
+ if (lockAuxView_ && auxDown_ && e.down() && !e.held())
+ {
+ // unlock aux lock when pressing aux again
+ lockAuxView_ = false;
+ auxDown_ = true;
+ }
+ else if (lockAuxView_ && auxDown_ && !e.down())
+ {
+ auxDown_ = true; // Aux Down stays valid until pushed again
+ }
+ else
+ {
+ auxDown_ = e.down();
+ }
+
+ omxLeds.setDirty();
+ omxDisp.setDirty();
+ return;
+ }
+
+ // key is on the right side
+ bool keyOnRight = (thisKey >= 6 && thisKey <= 10) || (thisKey >= 19);
+
+ if(auxDown_ && !keyOnRight)
+ {
+ if (!e.held())
+ {
+ if (e.down())
+ {
+ if (thisKey >= 1 && thisKey <= 5)
+ {
+ uint8_t paramIndex = thisKey - 1;
+ auto activeBank = getActiveBank();
+
+ if (activeBank->HasParamAtIndex(paramIndex))
+ {
+ activeParam = paramIndex;
+
+ omxLeds.setDirty();
+ omxDisp.setDirty();
+ }
+ }
+ // Change Octave
+ else if (thisKey == 11 || thisKey == 12)
+ {
+ int amt = thisKey == 11 ? -1 : 1;
+ midiSettings.octave = constrain(midiSettings.octave + amt, -5, 4);
+ }
+ // Lock AUX
+ else if (thisKey == 14)
+ {
+ lockAuxView_ = !lockAuxView_;
+
+ if(lockAuxView_)
+ {
+ omxDisp.displayMessage("Locked AUX");
+ }
+ else if(midiSettings.keyState[0] == false)
+ {
+ auxDown_ = false;
+ }
+ }
+ // Revert Values
+ else if(thisKey == 18)
+ {
+ omxDisp.displayMessage("Revert Vals");
+ revertPotPickups();
+ }
+ }
+ else
+ {
+ }
+ }
+
+ return;
+ }
+
+ if (!e.held())
+ {
+ // Keyboard on right for playing notes
+ if (keyOnRight)
+ {
+ if (e.down())
+ {
+ DoNoteOn(thisKey);
+ }
+ else
+ {
+ DoNoteOff(thisKey);
+ }
+ }
+ else
+ {
+ if (e.down())
+ {
+ // omxDisp.displayMessageTimed("Key Down " + String(thisKey), 5);
+
+ keyDownBankShortcut(thisKey);
+ // if (thisKey == keyEnv1_)
+ // {
+ // params_.setSelPage(DELPAGE_ENV1);
+ // }
+ // else if (thisKey == keyEnv2_)
+ // {
+ // params_.setSelPage(DELPAGE_ENV2);
+ // }
+ // else if (thisKey == keyFilt1_)
+ // {
+ // params_.setSelPage(DELPAGE_FILT1);
+ // }
+ // else if (thisKey == keyFilt2_)
+ // {
+ // params_.setSelPage(DELPAGE_FILT2);
+ // }
+ }
+ else
+ {
+ }
+ }
+ }
+
+ omxLeds.setDirty();
+ }
+
+ void MidiMacroDeluge::drawLEDs()
+ {
+ // omxLeds.updateBlinkStates();
+
+ if (omxLeds.isDirty() == false)
+ {
+ return;
+ }
+
+ auto blinkState = omxLeds.getBlinkState();
+
+ omxLeds.setAllLEDS(0, 0, 0);
+
+ strip.setPixelColor(0, (auxDown_ && blinkState) ? WHITE : BLUE); // aux
+
+ // strip.setPixelColor(but1_, midiSettings.keyState[but1_] ? LTYELLOW : ORANGE);
+ // strip.setPixelColor(but2_, midiSettings.keyState[but2_] ? LTYELLOW : ORANGE);
+ // strip.setPixelColor(but3_, midiSettings.keyState[but3_] ? LTYELLOW : ORANGE);
+
+ // strip.setPixelColor(enc1_, RED);
+ // strip.setPixelColor(enc2_, RED);
+ // strip.setPixelColor(enc3_, RED);
+
+ // strip.setPixelColor(keyUp_, midiSettings.keyState[keyUp_] ? LTCYAN : BLUE);
+ // strip.setPixelColor(keyDown_, midiSettings.keyState[keyDown_] ? LTCYAN : BLUE);
+ // strip.setPixelColor(keyLeft_, midiSettings.keyState[keyLeft_] ? LTCYAN : BLUE);
+ // strip.setPixelColor(keyRight_, midiSettings.keyState[keyRight_] ? LTCYAN : BLUE);
+
+ for (int q = 6; q < LED_COUNT; q++)
+ {
+ if ((q >= 6 && q <= 10) || (q >= 19))
+ {
+ if (midiSettings.midiKeyState[q] == -1)
+ {
+ if (colorConfig.midiBg_Hue == 0)
+ {
+ strip.setPixelColor(q, omxLeds.getKeyColor(scale_, q)); // set off or in scale
+ }
+ else if (colorConfig.midiBg_Hue == 32)
+ {
+ strip.setPixelColor(q, LOWWHITE);
+ }
+ else
+ {
+ strip.setPixelColor(q, strip.ColorHSV(colorConfig.midiBg_Hue, colorConfig.midiBg_Sat, colorConfig.midiBg_Brightness));
+ }
+ }
+ else
+ {
+ strip.setPixelColor(q, MIDINOTEON);
+ }
+ }
+ }
+
+ if (auxDown_)
+ {
+ for (int8_t i = 1; i <= 5; i++)
+ {
+ int8_t pIndex = i - 1;
+ auto activeBank = getActiveBank();
+
+ if (activeBank->HasParamAtIndex(pIndex))
+ {
+ strip.setPixelColor(i, pIndex == activeParam ? WHITE : ORANGE);
+ }
+ }
+
+ omxLeds.drawOctaveKeys(11, 12, midiSettings.octave);
+
+ strip.setPixelColor(14, lockAuxView_ ? PINK : MAGENTA);
+
+ // Revert Values Key
+ strip.setPixelColor(18, midiSettings.keyState[18] ? LTCYAN : RED);
+
+ }
+ else
+ {
+ for (int8_t i = 0; i < kNumBanks; i++)
+ {
+ if (i == selBank)
+ {
+ strip.setPixelColor(paramBanks[i].keyMap, paramBanks[i].altBank ? LTYELLOW : WHITE);
+ }
+ else
+ {
+ if (paramBanks[i].altBank == false)
+ {
+ strip.setPixelColor(paramBanks[i].keyMap, paramBanks[i].keyColor);
+ }
+ }
+ }
+ }
+ }
+
+ void MidiMacroDeluge::onEncoderChangedSelectParam(Encoder::Update enc)
+ {
+ params_.changeParam(enc.dir());
+
+ if(params_.getSelPage() == 0)
+ {
+ activeParam = params_.getSelParam();
+ }
+
+ omxDisp.setDirty();
+ }
+
+ void MidiMacroDeluge::onEncoderChangedEditParam(Encoder::Update enc)
+ {
+ // int8_t page = params_.getSelPage();
+ // // int8_t param = params_.getSelParam();
+
+ // // auto amt = enc.accel(5);
+
+ // uint8_t encCC = 0;
+
+ // if (page == NRNPAGE_ENC1)
+ // encCC = ccEnc1_;
+ // else if (page == NRNPAGE_ENC2)
+ // encCC = ccEnc2_;
+ // else if (page == NRNPAGE_ENC3)
+ // encCC = ccEnc3_;
+
+ // if (enc.dir() > 0)
+ // {
+ // MM::sendControlChange(encCC, 65, midiMacroConfig.midiMacroChan);
+ // }
+ // else if (enc.dir() < 0)
+ // {
+ // MM::sendControlChange(encCC, 63, midiMacroConfig.midiMacroChan);
+ // }
+
+ auto amt = enc.accel(1);
+
+ auto activeBank = getActiveBank();
+
+ if(activeBank->HasParamAtIndex(activeParam))
+ {
+ uint8_t newValue = constrain(potPickups[activeParam].value + amt, 0, 127);
+ potPickups[activeParam].SetVal(newValue, false);
+ activeBank->UpdatePotValue(activeParam, potPickups[activeParam].value);
+ MM::sendControlChange(activeBank->midiCCs[activeParam], potPickups[activeParam].value, midiMacroConfig.midiMacroChan);
+ }
+
+ omxDisp.setDirty();
+ }
+
+ void MidiMacroDeluge::onDisplayUpdate()
+ {
+ omxDisp.clearLegends();
+
+ auto activeBank = getActiveBank();
+
+ if(activeBank != nullptr)
+ {
+ // auto activeBank = getActiveBank();
+
+ // if (activeBank != nullptr)
+ // {
+ // uint8_t cc = activeBank->midiCCs[potIndex];
+
+ // if (cc <= 127 && newValue != prevValue)
+ // {
+ // potPickups[potIndex].UpdatePot(prevValue, newValue);
+ // activeBank->UpdatePotValue(potIndex, potPickups[potIndex].value);
+
+ // activeParam = potIndex;
+
+ // if(potPickups[potIndex].pickedUp)
+ // {
+ // // omxDisp.displayMessageTimed(String(activeBank->paramNames[potIndex]) + " " + String(cc) + " " + String(potPickups[potIndex].value), 5);
+
+ // uint8_t delugeMapVal = (uint8_t)map(potPickups[potIndex].value, 0, 127, 0, 50);
+
+ // // omxDisp.displayMessageTimed(String(activeBank->paramNames[potIndex]) + " " + String(delugeMapVal), 5);
+ // MM::sendControlChange(cc, potPickups[potIndex].value, midiMacroConfig.midiMacroChan);
+ // }
+ // else
+ // {
+ // uint8_t delugeMapNewVal = (uint8_t)map(newValue, 0, 127, 0, 50);
+ // uint8_t delugeMapVal = (uint8_t)map(potPickups[potIndex].value, 0, 127, 0, 50);
+ // // omxDisp.displayMessageTimed(String(delugeMapNewVal) + " -> " + String(delugeMapVal), 5);
+ // }
+ // }
+ // }
+
+ uint8_t delugeMapPotVal = (uint8_t)map(potPickups[activeParam].potValue, 0, 127, 0, 50);
+ uint8_t delugeMapVal = (uint8_t)map(potPickups[activeParam].value, 0, 127, 0, 50);
+
+ omxDisp.dispParamBar(delugeMapPotVal, delugeMapVal, 0, 50, potPickups[activeParam].pickedUp, false, activeBank->bankName, activeBank->paramNames[activeParam]);
+
+ // omxDisp.dispGenericModeLabel(activeBank->bankName, params_.getNumPages(), params_.getSelPage());
+ }
+ else
+ {
+ omxDisp.dispGenericMode2(params_.getNumPages(), params_.getSelPage(), params_.getSelParam(), encoderSelect_);
+ }
+
+ // int8_t page = params_.getSelPage();
+
+ // bool genericDisp = true;
+
+ // switch (page)
+ // {
+ // case DELPAGE_FILT1:
+ // case DELPAGE_FILT2:
+ // case DELPAGE_ENV1:
+ // case DELPAGE_ENV2:
+ // {
+ // omxDisp.dispGenericModeLabel(pageNames[page], params_.getNumPages(), params_.getSelPage());
+ // genericDisp = false;
+ // }
+ // break;
+ // default:
+ // break;
+ // }
+
+ // if (genericDisp)
+ // {
+ // omxDisp.dispGenericMode2(params_.getNumPages(), params_.getSelPage(), params_.getSelParam(), encoderSelect_);
+ // }
+ }
+}
diff --git a/Archive/OMX-27-firmware/src/midimacro/midimacro_deluge.h b/Archive/OMX-27-firmware/src/midimacro/midimacro_deluge.h
new file mode 100644
index 00000000..a0bec296
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/midimacro/midimacro_deluge.h
@@ -0,0 +1,146 @@
+#pragma once
+#include "midimacro_interface.h"
+#include "../utils/PotPickupUtil.h"
+
+// RAM: [===== ] 51.2% (used 33528 bytes from 65536 bytes)
+// Flash: [====== ] 61.5% (used 161276 bytes from 262144 bytes)
+// RAM: [===== ] 51.4% (used 33696 bytes from 65536 bytes)
+// Flash: [====== ] 61.6% (used 161356 bytes from 262144 bytes)
+
+namespace midimacro
+{
+ struct MidiCommand
+ {
+ const char *name;
+ uint8_t midiCC;
+ uint8_t keyMap;
+ int keyColorOn = WHITE;
+ int keyColorOff = ORANGE;
+ };
+
+ struct MidiParamBank
+ {
+ uint8_t keyMap;
+ int keyColor = ORANGE;
+ const char *bankName;
+ uint8_t midiCCs[5] = {255, 255, 255, 255, 255};
+ uint8_t midiValues[5] = {0, 0, 0, 0, 0};
+ const char *paramNames[5];
+ bool altBank = false;
+ uint8_t activeParam = 0;
+
+ void SetCCs(
+ const char *nameA = "", uint8_t a = 255,
+ const char *nameB = "", uint8_t b = 255,
+ const char *nameC = "", uint8_t c = 255,
+ const char *nameD = "", uint8_t d = 255,
+ const char *nameE = "VOL", uint8_t e = 7)
+ {
+ SetCC(0, nameA, a);
+ SetCC(1, nameB, b);
+ SetCC(2, nameC, c);
+ SetCC(3, nameD, d);
+ SetCC(4, nameE, e);
+ }
+
+ bool HasParamAtIndex(uint8_t index)
+ {
+ if(index >= 5) return false;
+
+ return midiCCs[index] < 128;
+ }
+
+ void SetCC(uint8_t index, const char *name, uint8_t cc)
+ {
+ if (index >= 5)
+ return;
+ midiCCs[index] = cc;
+ paramNames[index] = name;
+ }
+
+ int8_t ContainsCC(uint8_t cc)
+ {
+ for(int8_t i = 0; i < 5; i++)
+ {
+ if(midiCCs[i] == cc)
+ {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ int8_t UpdateCCValue(uint8_t cc, uint8_t value)
+ {
+ int8_t index = ContainsCC(cc);
+ if(index < 0) return index;
+ midiValues[index] = value;
+ return index;
+ }
+
+ void UpdatePotValue(uint8_t potIndex, uint8_t value)
+ {
+ midiValues[potIndex] = value;
+ }
+ };
+
+ class MidiMacroDeluge : public MidiMacroInterface
+ {
+ public:
+ MidiMacroDeluge();
+ ~MidiMacroDeluge() {}
+
+ bool consumesPots() override { return true; }
+ bool consumesDisplay() override { return true; }
+
+ String getName() override;
+
+ void loopUpdate() override;
+
+ void onDisplayUpdate() override;
+
+ void onPotChanged(int potIndex, int prevValue, int newValue, int analogDelta) override;
+ void onKeyUpdate(OMXKeypadEvent e) override;
+ void drawLEDs() override;
+
+ void inMidiControlChange(byte channel, byte control, byte value) override;
+ protected:
+ void onEnabled() override;
+ void onDisabled() override;
+
+ void onEncoderChangedSelectParam(Encoder::Update enc) override;
+ void onEncoderChangedEditParam(Encoder::Update enc) override;
+
+ private:
+ static const uint8_t kNumBanks = 16;
+
+ MidiParamBank* getActiveBank();
+ void keyDownBankShortcut(uint8_t keyIndex);
+ void setActiveBank(uint8_t bankIndex);
+ void updatePotPickups();
+ void revertPotPickups();
+
+
+ MidiParamBank paramBanks[kNumBanks];
+ // Maps each CC to a cached value
+ uint8_t delVals[127];
+
+ bool auxDown_ = false;
+ bool lockAuxView_ = false;
+ bool commandMode_ = false;
+
+
+ uint8_t selBank = 0;
+ uint8_t activeParam = 0;
+
+ PotPickupUtil potPickups[5];
+
+ // bool m8mutesolo_[16];
+
+ // Control key mappings
+ // static const uint8_t keyFilt1_ = 3;
+ // static const uint8_t keyFilt2_ = 4;
+ // static const uint8_t keyEnv1_ = 1;
+ // static const uint8_t keyEnv2_ = 2;
+ };
+}
diff --git a/Archive/OMX-27-firmware/src/midimacro/midimacro_interface.cpp b/Archive/OMX-27-firmware/src/midimacro/midimacro_interface.cpp
new file mode 100644
index 00000000..73e566bb
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/midimacro/midimacro_interface.cpp
@@ -0,0 +1,86 @@
+#include "midimacro_interface.h"
+#include "../hardware/omx_disp.h"
+namespace midimacro
+{
+ MidiMacroInterface::~MidiMacroInterface()
+ {
+ // std::vector().swap(triggeredNotes);
+ // Serial.println("Deleted vector");
+ }
+
+ void MidiMacroInterface::setEnabled(bool newEnabled)
+ {
+ enabled_ = newEnabled;
+ if (enabled_)
+ {
+ onEnabled();
+ }
+ else
+ {
+ onDisabled();
+ }
+ }
+
+ bool MidiMacroInterface::getEnabled()
+ {
+ return enabled_;
+ }
+
+ void MidiMacroInterface::onEncoderChanged(Encoder::Update enc)
+ {
+ if (encoderSelect_)
+ {
+ onEncoderChangedSelectParam(enc);
+ }
+ else
+ {
+ onEncoderChangedEditParam(enc);
+ }
+ }
+
+ // Handles selecting params using encoder
+ void MidiMacroInterface::onEncoderChangedSelectParam(Encoder::Update enc)
+ {
+ params_.changeParam(enc.dir());
+ omxDisp.setDirty();
+ }
+
+ void MidiMacroInterface::onEncoderButtonDown()
+ {
+ encoderSelect_ = !encoderSelect_;
+ omxDisp.setDirty();
+ }
+
+ void MidiMacroInterface::setScale(MusicScales *scale)
+ {
+ scale_ = scale;
+ }
+
+ void MidiMacroInterface::setDoNoteOn(void (*fptr)(void *, uint8_t), void *context)
+ {
+ doNoteOnFptrContext_ = context;
+ doNoteOnFptr_ = fptr;
+ }
+
+ void MidiMacroInterface::setDoNoteOff(void (*fptr)(void *, uint8_t), void *context)
+ {
+ doNoteOffFptrContext_ = context;
+ doNoteOffFptr_ = fptr;
+ }
+
+ void MidiMacroInterface::DoNoteOn(uint8_t keyIndex)
+ {
+ if (doNoteOnFptrContext_ != nullptr)
+ {
+ doNoteOnFptr_(doNoteOnFptrContext_, keyIndex);
+ }
+ }
+
+ void MidiMacroInterface::DoNoteOff(uint8_t keyIndex)
+ {
+ if (doNoteOffFptrContext_ != nullptr)
+ {
+ doNoteOffFptr_(doNoteOffFptrContext_, keyIndex);
+ }
+ }
+}
diff --git a/Archive/OMX-27-firmware/src/midimacro/midimacro_interface.h b/Archive/OMX-27-firmware/src/midimacro/midimacro_interface.h
new file mode 100644
index 00000000..e0b446a8
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/midimacro/midimacro_interface.h
@@ -0,0 +1,68 @@
+#pragma once
+#include "../config.h"
+#include "../ClearUI/ClearUI_Input.h"
+#include "../hardware/omx_keypad.h"
+#include "../utils/param_manager.h"
+#include "../utils/music_scales.h"
+
+namespace midimacro
+{
+ class MidiMacroInterface
+ {
+ public:
+ MidiMacroInterface() {}
+ virtual ~MidiMacroInterface();
+
+ // Return true if consumes pots
+ virtual bool consumesPots() = 0;
+
+ // Return true if consumes display / encoder
+ virtual bool consumesDisplay() = 0;
+
+ // Display name
+ virtual String getName() = 0;
+
+ virtual void setEnabled(bool newEnabled);
+ virtual bool getEnabled();
+
+ virtual void loopUpdate() {}
+
+ virtual void onEncoderChanged(Encoder::Update enc);
+ virtual void onEncoderButtonDown();
+
+ virtual void onDisplayUpdate() = 0;
+
+ virtual void onPotChanged(int potIndex, int prevValue, int newValue, int analogDelta) = 0;
+ virtual void onKeyUpdate(OMXKeypadEvent e) = 0;
+ virtual void drawLEDs() = 0;
+
+ virtual void setScale(MusicScales *scale);
+
+ virtual void setDoNoteOn(void (*fptr)(void *, uint8_t), void *context);
+ virtual void setDoNoteOff(void (*fptr)(void *, uint8_t), void *context);
+
+ virtual void inMidiControlChange(byte channel, byte control, byte value) {}
+
+ protected:
+ bool enabled_;
+ bool encoderSelect_;
+ ParamManager params_;
+
+ MusicScales *scale_;
+
+ void *doNoteOnFptrContext_;
+ void (*doNoteOnFptr_)(void *, uint8_t);
+
+ void *doNoteOffFptrContext_;
+ void (*doNoteOffFptr_)(void *, uint8_t);
+
+ virtual void onEnabled() {} // Called whenever entering mode
+ virtual void onDisabled() {} // Called whenever entering mode
+
+ virtual void onEncoderChangedSelectParam(Encoder::Update enc);
+ virtual void onEncoderChangedEditParam(Encoder::Update enc) = 0;
+
+ virtual void DoNoteOn(uint8_t keyIndex);
+ virtual void DoNoteOff(uint8_t keyIndex);
+ };
+}
diff --git a/Archive/OMX-27-firmware/src/midimacro/midimacro_m8.cpp b/Archive/OMX-27-firmware/src/midimacro/midimacro_m8.cpp
new file mode 100644
index 00000000..5502dce4
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/midimacro/midimacro_m8.cpp
@@ -0,0 +1,466 @@
+#include "midimacro_m8.h"
+#include "../utils/omx_util.h"
+#include "../hardware/omx_disp.h"
+#include "../hardware/omx_leds.h"
+#include "../midi/midi.h"
+#include "../consts/consts.h"
+#include "../consts/colors.h"
+namespace midimacro
+{
+ enum M8Page
+ {
+ M8PAGE_MUTESOLO,
+ M8PAGE_CONTROL
+ };
+
+ MidiMacroM8::MidiMacroM8()
+ {
+ params_.addPage(1); // Mute / Solo
+ params_.addPage(1); // Control
+ encoderSelect_ = true;
+
+ for (uint8_t i = 0; i < 16; i++)
+ {
+ m8mutesolo_[i] = false;
+ }
+ }
+
+ String MidiMacroM8::getName()
+ {
+ return String("M8");
+ }
+
+ void MidiMacroM8::onEnabled()
+ {
+ }
+
+ void MidiMacroM8::onDisabled()
+ {
+ }
+
+ void MidiMacroM8::loopUpdate()
+ {
+ }
+
+ void MidiMacroM8::onPotChanged(int potIndex, int prevValue, int newValue, int analogDelta)
+ {
+ omxUtil.sendPots(potIndex, midiMacroConfig.midiMacroChan);
+ }
+
+ void MidiMacroM8::onEncoderButtonDown()
+ {
+ encoderSelect_ = true;
+ // encoderSelect_ = !encoderSelect_;
+ omxDisp.setDirty();
+ }
+
+ void MidiMacroM8::onKeyUpdate(OMXKeypadEvent e)
+ {
+ int thisKey = e.key();
+ int keyPos = thisKey - 11;
+
+ int8_t page = params_.getSelPage();
+
+ if (page == M8PAGE_MUTESOLO)
+ {
+ if (!e.held())
+ {
+ if (thisKey == 8)
+ {
+ if (e.down())
+ {
+ omxDisp.displayMessage("Shift");
+ MM::sendNoteOn(1, 1, midiMacroConfig.midiMacroChan); // Shift
+ }
+ else
+ {
+ MM::sendNoteOff(1, 0, midiMacroConfig.midiMacroChan); // Shift
+ }
+ }
+ }
+ if (!e.held())
+ {
+ if (e.down() && (thisKey > 10 && thisKey < 27))
+ {
+ // Mutes / Solos
+ m8mutesolo_[keyPos] = !m8mutesolo_[keyPos];
+ int mutePos = keyPos + 12;
+ if (m8mutesolo_[keyPos])
+ {
+ if (keyPos < 8)
+ {
+ omxDisp.displayMessage("Mute");
+ }
+ else
+ {
+ omxDisp.displayMessage("Solo");
+ }
+ MM::sendNoteOn(mutePos, 1, midiMacroConfig.midiMacroChan);
+ }
+ else
+ {
+ MM::sendNoteOff(mutePos, 0, midiMacroConfig.midiMacroChan);
+ }
+ return; // break;
+ }
+ else if (e.down() && (thisKey == 1))
+ {
+ omxDisp.displayMessage("Unmute all");
+ // release all mutes
+ for (int z = 0; z < 8; z++)
+ {
+ int mutePos = z + 12;
+ if (m8mutesolo_[z])
+ {
+ m8mutesolo_[z] = false;
+ MM::sendNoteOff(mutePos, 0, midiMacroConfig.midiMacroChan);
+ }
+ }
+ return; // break;
+ }
+ else if (e.down() && (thisKey == 2))
+ {
+ // ?
+ return; // break;
+ }
+ else if (e.down() && (thisKey == 3))
+ {
+ omxDisp.displayMessage("Goto Mixer");
+ // return to mixer
+ // hold shift 4 left 1 down, release shift
+ MM::sendNoteOn(1, 1, midiMacroConfig.midiMacroChan); // Shift
+ delay(40);
+ MM::sendNoteOn(6, 1, midiMacroConfig.midiMacroChan); // Up
+ delay(20);
+ MM::sendNoteOff(6, 0, midiMacroConfig.midiMacroChan);
+ delay(40);
+ MM::sendNoteOn(4, 1, midiMacroConfig.midiMacroChan); // Left
+ delay(20);
+ MM::sendNoteOff(4, 0, midiMacroConfig.midiMacroChan);
+ delay(40);
+ MM::sendNoteOn(4, 1, midiMacroConfig.midiMacroChan); // Left
+ delay(20);
+ MM::sendNoteOff(4, 0, midiMacroConfig.midiMacroChan);
+ delay(40);
+ MM::sendNoteOn(4, 1, midiMacroConfig.midiMacroChan); // Left
+ delay(20);
+ MM::sendNoteOff(4, 0, midiMacroConfig.midiMacroChan);
+ delay(40);
+ MM::sendNoteOn(4, 1, midiMacroConfig.midiMacroChan); // Left
+ delay(20);
+ MM::sendNoteOff(4, 0, midiMacroConfig.midiMacroChan);
+ delay(40);
+ MM::sendNoteOn(7, 1, midiMacroConfig.midiMacroChan); // Down
+ delay(20);
+ MM::sendNoteOff(7, 0, midiMacroConfig.midiMacroChan);
+ MM::sendNoteOff(1, 0, midiMacroConfig.midiMacroChan);
+ omxDisp.displayMessage("Goto Mixer");
+
+ return; // break;
+ }
+ else if (e.down() && (thisKey == 4))
+ {
+ omxDisp.displayMessage("Save snapshot");
+ // snap save
+ MM::sendNoteOn(1, 1, midiMacroConfig.midiMacroChan); // Shift
+ delay(40);
+ MM::sendNoteOn(3, 1, midiMacroConfig.midiMacroChan); // Option
+ delay(40);
+ MM::sendNoteOff(3, 0, midiMacroConfig.midiMacroChan);
+ MM::sendNoteOff(1, 0, midiMacroConfig.midiMacroChan);
+
+ return; // break;
+ }
+ else if (e.down() && (thisKey == 5))
+ {
+ omxDisp.displayMessage("Load snapshot");
+ // snap load
+ MM::sendNoteOn(1, 1, midiMacroConfig.midiMacroChan); // Shift
+ delay(40);
+ MM::sendNoteOn(2, 1, midiMacroConfig.midiMacroChan); // Edit
+ delay(40);
+ MM::sendNoteOff(2, 0, midiMacroConfig.midiMacroChan);
+ MM::sendNoteOff(1, 0, midiMacroConfig.midiMacroChan);
+
+ // then reset mutes/solos
+ for (int z = 0; z < 16; z++)
+ {
+ if (m8mutesolo_[z])
+ {
+ m8mutesolo_[z] = false;
+ }
+ }
+
+ return; // break;
+ }
+ else if (e.down() && (thisKey == 6))
+ {
+ omxDisp.displayMessage("Unsolo all");
+ // release all solos
+ for (int z = 8; z < 16; z++)
+ {
+ int mutePos = z + 12;
+ if (m8mutesolo_[z])
+ {
+ m8mutesolo_[z] = false;
+ MM::sendNoteOff(mutePos, 0, midiMacroConfig.midiMacroChan);
+ }
+ }
+ return; // break;
+ }
+ else if (e.down() && (thisKey == 7))
+ {
+ // ??
+ return; // break;
+ }
+ else if (e.down() && (thisKey == 8))
+ {
+ // omxDisp.displayMessage("Reset Pat");
+ // MM::sendNoteOn(1, 1, midiMacroConfig.midiMacroChan); // Shift
+ return; // break;
+ }
+ else if (e.down() && (thisKey == 9))
+ {
+ omxDisp.displayMessage("Waveform");
+ // waveform
+ MM::sendNoteOn(6, 1, midiMacroConfig.midiMacroChan); // Up
+ MM::sendNoteOn(7, 1, midiMacroConfig.midiMacroChan); // Down
+ MM::sendNoteOn(5, 1, midiMacroConfig.midiMacroChan); // Right
+ MM::sendNoteOn(4, 1, midiMacroConfig.midiMacroChan); // Left
+ delay(40);
+
+ MM::sendNoteOff(6, 0, midiMacroConfig.midiMacroChan); // Up
+ MM::sendNoteOff(7, 0, midiMacroConfig.midiMacroChan); // Down
+ MM::sendNoteOff(5, 0, midiMacroConfig.midiMacroChan); // Right
+ MM::sendNoteOff(4, 0, midiMacroConfig.midiMacroChan); // Left
+
+ return; // break;
+ }
+ else if (e.down() && (thisKey == 10))
+ {
+ omxDisp.displayMessage("Play");
+ // play
+ MM::sendNoteOn(0, 1, midiMacroConfig.midiMacroChan); // Play
+ delay(40);
+ MM::sendNoteOff(0, 0, midiMacroConfig.midiMacroChan); // Play
+
+ // MM::sendNoteOn(1, 1, midiMacroChan); // Shift
+ // MM::sendNoteOn(3, 1, midiMacroChan); // Option
+ // MM::sendNoteOn(2, 1, midiMacroChan); // Edit
+ // MM::sendNoteOn(6, 1, midiMacroChan); // Up
+ // MM::sendNoteOn(7, 1, midiMacroChan); // Down
+ // MM::sendNoteOn(4, 1, midiMacroChan); // Left
+ // MM::sendNoteOn(5, 1, midiMacroChan); // Right
+ return; // break;
+ }
+ }
+ }
+ else if (page == M8PAGE_CONTROL)
+ {
+ if (thisKey != 0 && !e.held())
+ {
+ if ((thisKey >= 6 && thisKey <= 10) || (thisKey >= 19))
+ {
+ if (e.down())
+ {
+ DoNoteOn(thisKey);
+ }
+ else
+ {
+ DoNoteOff(thisKey);
+ }
+ }
+ else
+ {
+
+ if (e.down())
+ {
+ if (thisKey == keyUp_)
+ MM::sendNoteOn(6, 1, midiMacroConfig.midiMacroChan);
+ if (thisKey == keyDown_)
+ MM::sendNoteOn(7, 1, midiMacroConfig.midiMacroChan);
+ if (thisKey == keyLeft_)
+ MM::sendNoteOn(4, 1, midiMacroConfig.midiMacroChan);
+ if (thisKey == keyRight_)
+ MM::sendNoteOn(5, 1, midiMacroConfig.midiMacroChan);
+
+ if (thisKey == keyOption_)
+ MM::sendNoteOn(3, 1, midiMacroConfig.midiMacroChan);
+ if (thisKey == keyEdit_)
+ MM::sendNoteOn(2, 1, midiMacroConfig.midiMacroChan);
+
+ if (thisKey == keyShift_)
+ MM::sendNoteOn(1, 1, midiMacroConfig.midiMacroChan);
+ if (thisKey == keyPlay_)
+ MM::sendNoteOn(0, 1, midiMacroConfig.midiMacroChan);
+ }
+ else
+ {
+ if (thisKey == keyUp_)
+ MM::sendNoteOff(6, 0, midiMacroConfig.midiMacroChan);
+ if (thisKey == keyDown_)
+ MM::sendNoteOff(7, 0, midiMacroConfig.midiMacroChan);
+ if (thisKey == keyLeft_)
+ MM::sendNoteOff(4, 0, midiMacroConfig.midiMacroChan);
+ if (thisKey == keyRight_)
+ MM::sendNoteOff(5, 0, midiMacroConfig.midiMacroChan);
+
+ if (thisKey == keyOption_)
+ MM::sendNoteOff(3, 0, midiMacroConfig.midiMacroChan);
+ if (thisKey == keyEdit_)
+ MM::sendNoteOff(2, 0, midiMacroConfig.midiMacroChan);
+
+ if (thisKey == keyShift_)
+ MM::sendNoteOff(1, 0, midiMacroConfig.midiMacroChan);
+ if (thisKey == keyPlay_)
+ MM::sendNoteOff(0, 0, midiMacroConfig.midiMacroChan);
+ }
+ }
+ }
+ }
+
+ omxLeds.setDirty();
+ }
+
+ void MidiMacroM8::drawLEDs()
+ {
+ // omxLeds.updateBlinkStates();
+
+ if (omxLeds.isDirty() == false)
+ {
+ return;
+ }
+
+ auto blinkState = omxLeds.getBlinkState();
+
+ omxLeds.setAllLEDS(0, 0, 0);
+
+ int8_t page = params_.getSelPage();
+
+ if (page == M8PAGE_MUTESOLO)
+ {
+ auto color5 = blinkState ? ORANGE : LEDOFF;
+ auto color6 = blinkState ? RED : LEDOFF;
+
+ strip.setPixelColor(0, BLUE);
+ strip.setPixelColor(1, ORANGE); // all mute
+ strip.setPixelColor(3, LIME); // MIXER
+ strip.setPixelColor(4, CYAN); // snap load
+ strip.setPixelColor(5, MAGENTA); // snap save
+
+ for (int m = 11; m < LED_COUNT - 8; m++)
+ {
+ if (m8mutesolo_[m - 11])
+ {
+ strip.setPixelColor(m, color5);
+ }
+ else
+ {
+ strip.setPixelColor(m, ORANGE);
+ }
+ }
+
+ strip.setPixelColor(6, RED); // all solo
+ for (int m = 19; m < LED_COUNT; m++)
+ {
+ if (m8mutesolo_[m - 11])
+ {
+ strip.setPixelColor(m, color6);
+ }
+ else
+ {
+ strip.setPixelColor(m, RED);
+ }
+ }
+ strip.setPixelColor(2, LEDOFF);
+ strip.setPixelColor(7, LEDOFF);
+ // strip.setPixelColor(8, LEDOFF);
+ strip.setPixelColor(8, PINK); // snap save
+
+ strip.setPixelColor(9, YELLOW); // WAVES
+ strip.setPixelColor(10, BLUE); // PLAY
+ }
+ else if (page == M8PAGE_CONTROL)
+ {
+ strip.setPixelColor(0, BLUE); // aux
+
+ strip.setPixelColor(keyUp_, ORANGE); // up
+ strip.setPixelColor(keyDown_, ORANGE); // down
+ strip.setPixelColor(keyLeft_, RED); // left
+ strip.setPixelColor(keyRight_, RED); // right
+
+ strip.setPixelColor(keyOption_, BLUE); // option
+ strip.setPixelColor(keyEdit_, BLUE); // edit
+ strip.setPixelColor(keyShift_, GREEN); // shift
+ strip.setPixelColor(keyPlay_, GREEN); // play
+
+ for (int q = 1; q < LED_COUNT; q++)
+ {
+ if ((q >= 6 && q <= 10) || (q >= 19))
+ {
+ if (midiSettings.midiKeyState[q] == -1)
+ {
+ if (colorConfig.midiBg_Hue == 0)
+ {
+ strip.setPixelColor(q, omxLeds.getKeyColor(scale_, q)); // set off or in scale
+ }
+ else if (colorConfig.midiBg_Hue == 32)
+ {
+ strip.setPixelColor(q, LOWWHITE);
+ }
+ else
+ {
+ strip.setPixelColor(q, strip.ColorHSV(colorConfig.midiBg_Hue, colorConfig.midiBg_Sat, colorConfig.midiBg_Brightness));
+ }
+ }
+ else
+ {
+ strip.setPixelColor(q, MIDINOTEON);
+ }
+ }
+ }
+ }
+ }
+
+ void MidiMacroM8::onEncoderChangedEditParam(Encoder::Update enc)
+ {
+ // int8_t page = params_.getSelPage();
+ // int8_t param = params_.getSelParam();
+
+ // auto amt = enc.accel(5);
+
+ omxDisp.setDirty();
+ }
+
+ void MidiMacroM8::onDisplayUpdate()
+ {
+ omxDisp.clearLegends();
+
+ int8_t page = params_.getSelPage();
+
+ bool genericDisp = true;
+
+ switch (page)
+ {
+ case M8PAGE_MUTESOLO:
+ {
+ omxDisp.dispGenericModeLabel("Mute Solo", params_.getNumPages(), params_.getSelPage());
+ genericDisp = false;
+ }
+ break;
+ case M8PAGE_CONTROL:
+ {
+ omxDisp.dispGenericModeLabel("Control", params_.getNumPages(), params_.getSelPage());
+ genericDisp = false;
+ }
+ break;
+ default:
+ break;
+ }
+
+ if (genericDisp)
+ {
+ omxDisp.dispGenericMode2(params_.getNumPages(), params_.getSelPage(), params_.getSelParam(), encoderSelect_);
+ }
+ }
+}
diff --git a/Archive/OMX-27-firmware/src/midimacro/midimacro_m8.h b/Archive/OMX-27-firmware/src/midimacro/midimacro_m8.h
new file mode 100644
index 00000000..8dbaad21
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/midimacro/midimacro_m8.h
@@ -0,0 +1,46 @@
+#pragma once
+#include "midimacro_interface.h"
+
+namespace midimacro
+{
+ class MidiMacroM8 : public MidiMacroInterface
+ {
+ public:
+ MidiMacroM8();
+ ~MidiMacroM8() {}
+
+ bool consumesPots() override { return true; }
+ bool consumesDisplay() override { return true; }
+
+ String getName() override;
+
+ void loopUpdate() override;
+
+ void onDisplayUpdate() override;
+
+ void onPotChanged(int potIndex, int prevValue, int newValue, int analogDelta) override;
+ void onEncoderButtonDown() override;
+ void onKeyUpdate(OMXKeypadEvent e) override;
+ void drawLEDs() override;
+
+ protected:
+ void onEnabled() override;
+ void onDisabled() override;
+
+ void onEncoderChangedEditParam(Encoder::Update enc) override;
+
+ private:
+ bool m8mutesolo_[16];
+
+ // M8PAGE_CONTROL key mappings
+ uint8_t keyUp_ = 1;
+ uint8_t keyDown_ = 12;
+ uint8_t keyLeft_ = 11;
+ uint8_t keyRight_ = 13;
+
+ uint8_t keyOption_ = 4;
+ uint8_t keyEdit_ = 5;
+ uint8_t keyShift_ = 16;
+ uint8_t keyPlay_ = 17;
+ };
+}
diff --git a/Archive/OMX-27-firmware/src/midimacro/midimacro_norns.cpp b/Archive/OMX-27-firmware/src/midimacro/midimacro_norns.cpp
new file mode 100644
index 00000000..5c72ccba
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/midimacro/midimacro_norns.cpp
@@ -0,0 +1,275 @@
+#include "midimacro_norns.h"
+#include "../utils/omx_util.h"
+#include "../hardware/omx_disp.h"
+#include "../hardware/omx_leds.h"
+#include "../midi/midi.h"
+#include "../consts/consts.h"
+#include "../consts/colors.h"
+
+namespace midimacro
+{
+ enum NornsPage
+ {
+ NRNPAGE_ENC1,
+ NRNPAGE_ENC2,
+ NRNPAGE_ENC3
+ };
+
+ MidiMacroNorns::MidiMacroNorns()
+ {
+ params_.addPage(1); // Enc1
+ params_.addPage(1); // Enc2
+ params_.addPage(1); // Enc3
+
+ encoderSelect_ = true;
+ }
+
+ String MidiMacroNorns::getName()
+ {
+ return String("NORNS");
+ }
+
+ void MidiMacroNorns::onEnabled()
+ {
+ }
+
+ void MidiMacroNorns::onDisabled()
+ {
+ }
+
+ void MidiMacroNorns::loopUpdate()
+ {
+ }
+
+ void MidiMacroNorns::onPotChanged(int potIndex, int prevValue, int newValue, int analogDelta)
+ {
+ omxUtil.sendPots(potIndex, midiMacroConfig.midiMacroChan);
+ }
+
+ void MidiMacroNorns::onKeyUpdate(OMXKeypadEvent e)
+ {
+ int thisKey = e.key();
+ // int keyPos = thisKey - 11;
+
+ if (thisKey != 0 && !e.held())
+ {
+ if ((thisKey >= 6 && thisKey <= 10) || (thisKey >= 19))
+ {
+ if (e.down())
+ {
+ DoNoteOn(thisKey);
+ }
+ else
+ {
+ DoNoteOff(thisKey);
+ }
+ }
+ else
+ {
+ if (e.down())
+ {
+ if (thisKey == but1_)
+ {
+ MM::sendControlChange(ccBut1_, 127, midiMacroConfig.midiMacroChan);
+ }
+ else if (thisKey == but2_)
+ {
+ MM::sendControlChange(ccBut2_, 127, midiMacroConfig.midiMacroChan);
+ }
+ else if (thisKey == but3_)
+ {
+ MM::sendControlChange(ccBut3_, 127, midiMacroConfig.midiMacroChan);
+ }
+ else if (thisKey == enc1_)
+ {
+ params_.setSelPageAndParam(0, 0);
+ encoderSelect_ = false;
+ omxDisp.setDirty();
+ }
+ else if (thisKey == enc2_)
+ {
+ params_.setSelPageAndParam(1, 0);
+ encoderSelect_ = false;
+ omxDisp.setDirty();
+ }
+ else if (thisKey == enc3_)
+ {
+ params_.setSelPageAndParam(2, 0);
+ encoderSelect_ = false;
+ omxDisp.setDirty();
+ }
+ else if (thisKey == keyUp_)
+ {
+ // params_.setSelPageAndParam(1,0);
+ // encoderSelect_ = false;
+ MM::sendControlChange(ccEnc2_, 63, midiMacroConfig.midiMacroChan);
+ delay(20);
+ MM::sendControlChange(ccEnc2_, 63, midiMacroConfig.midiMacroChan);
+ }
+ else if (thisKey == keyDown_)
+ {
+ // params_.setSelPageAndParam(1,0);
+ // encoderSelect_ = false;
+ MM::sendControlChange(ccEnc2_, 65, midiMacroConfig.midiMacroChan);
+ delay(20);
+ MM::sendControlChange(ccEnc2_, 65, midiMacroConfig.midiMacroChan);
+ }
+ else if (thisKey == keyLeft_)
+ {
+ // params_.setSelPageAndParam(2,0);
+ // encoderSelect_ = false;
+ MM::sendControlChange(ccEnc3_, 63, midiMacroConfig.midiMacroChan);
+ delay(20);
+ MM::sendControlChange(ccEnc3_, 63, midiMacroConfig.midiMacroChan);
+ }
+ else if (thisKey == keyRight_)
+ {
+ // params_.setSelPageAndParam(2,0);
+ // encoderSelect_ = false;
+ MM::sendControlChange(ccEnc3_, 65, midiMacroConfig.midiMacroChan);
+ delay(20);
+ MM::sendControlChange(ccEnc3_, 65, midiMacroConfig.midiMacroChan);
+ }
+ }
+ else
+ {
+ if (thisKey == but1_)
+ {
+ MM::sendControlChange(ccBut1_, 0, midiMacroConfig.midiMacroChan);
+ }
+ else if (thisKey == but2_)
+ {
+ MM::sendControlChange(ccBut2_, 0, midiMacroConfig.midiMacroChan);
+ }
+ else if (thisKey == but3_)
+ {
+ MM::sendControlChange(ccBut3_, 0, midiMacroConfig.midiMacroChan);
+ }
+ }
+ }
+ }
+
+ omxLeds.setDirty();
+ }
+
+ void MidiMacroNorns::drawLEDs()
+ {
+ // omxLeds.updateBlinkStates();
+
+ if (omxLeds.isDirty() == false)
+ {
+ return;
+ }
+
+ // auto blinkState = omxLeds.getBlinkState();
+
+ omxLeds.setAllLEDS(0, 0, 0);
+
+ strip.setPixelColor(0, BLUE); // aux
+
+ strip.setPixelColor(but1_, midiSettings.keyState[but1_] ? LTYELLOW : ORANGE);
+ strip.setPixelColor(but2_, midiSettings.keyState[but2_] ? LTYELLOW : ORANGE);
+ strip.setPixelColor(but3_, midiSettings.keyState[but3_] ? LTYELLOW : ORANGE);
+
+ strip.setPixelColor(enc1_, RED);
+ strip.setPixelColor(enc2_, RED);
+ strip.setPixelColor(enc3_, RED);
+
+ strip.setPixelColor(keyUp_, midiSettings.keyState[keyUp_] ? LTCYAN : BLUE);
+ strip.setPixelColor(keyDown_, midiSettings.keyState[keyDown_] ? LTCYAN : BLUE);
+ strip.setPixelColor(keyLeft_, midiSettings.keyState[keyLeft_] ? LTCYAN : BLUE);
+ strip.setPixelColor(keyRight_, midiSettings.keyState[keyRight_] ? LTCYAN : BLUE);
+
+ for (int q = 1; q < LED_COUNT; q++)
+ {
+ if ((q >= 6 && q <= 10) || (q >= 19))
+ {
+ if (midiSettings.midiKeyState[q] == -1)
+ {
+ if (colorConfig.midiBg_Hue == 0)
+ {
+ strip.setPixelColor(q, omxLeds.getKeyColor(scale_, q)); // set off or in scale
+ }
+ else if (colorConfig.midiBg_Hue == 32)
+ {
+ strip.setPixelColor(q, LOWWHITE);
+ }
+ else
+ {
+ strip.setPixelColor(q, strip.ColorHSV(colorConfig.midiBg_Hue, colorConfig.midiBg_Sat, colorConfig.midiBg_Brightness));
+ }
+ }
+ else
+ {
+ strip.setPixelColor(q, MIDINOTEON);
+ }
+ }
+ }
+ }
+
+ void MidiMacroNorns::onEncoderChangedEditParam(Encoder::Update enc)
+ {
+ int8_t page = params_.getSelPage();
+ // int8_t param = params_.getSelParam();
+
+ // auto amt = enc.accel(5);
+
+ uint8_t encCC = 0;
+
+ if (page == NRNPAGE_ENC1)
+ encCC = ccEnc1_;
+ else if (page == NRNPAGE_ENC2)
+ encCC = ccEnc2_;
+ else if (page == NRNPAGE_ENC3)
+ encCC = ccEnc3_;
+
+ if (enc.dir() > 0)
+ {
+ MM::sendControlChange(encCC, 65, midiMacroConfig.midiMacroChan);
+ }
+ else if (enc.dir() < 0)
+ {
+ MM::sendControlChange(encCC, 63, midiMacroConfig.midiMacroChan);
+ }
+
+ omxDisp.setDirty();
+ }
+
+ void MidiMacroNorns::onDisplayUpdate()
+ {
+ omxDisp.clearLegends();
+
+ int8_t page = params_.getSelPage();
+
+ bool genericDisp = true;
+
+ switch (page)
+ {
+ case NRNPAGE_ENC1:
+ {
+ omxDisp.dispGenericModeLabel("Enc 1", params_.getNumPages(), params_.getSelPage());
+ genericDisp = false;
+ }
+ break;
+ case NRNPAGE_ENC2:
+ {
+ omxDisp.dispGenericModeLabel("Enc 2", params_.getNumPages(), params_.getSelPage());
+ genericDisp = false;
+ }
+ break;
+ case NRNPAGE_ENC3:
+ {
+ omxDisp.dispGenericModeLabel("Enc 3", params_.getNumPages(), params_.getSelPage());
+ genericDisp = false;
+ }
+ break;
+ default:
+ break;
+ }
+
+ if (genericDisp)
+ {
+ omxDisp.dispGenericMode2(params_.getNumPages(), params_.getSelPage(), params_.getSelParam(), encoderSelect_);
+ }
+ }
+}
diff --git a/Archive/OMX-27-firmware/src/midimacro/midimacro_norns.h b/Archive/OMX-27-firmware/src/midimacro/midimacro_norns.h
new file mode 100644
index 00000000..ef41de6d
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/midimacro/midimacro_norns.h
@@ -0,0 +1,56 @@
+#pragma once
+#include "midimacro_interface.h"
+
+namespace midimacro
+{
+ class MidiMacroNorns : public MidiMacroInterface
+ {
+ public:
+ MidiMacroNorns();
+ ~MidiMacroNorns() {}
+
+ bool consumesPots() override { return true; }
+ bool consumesDisplay() override { return true; }
+
+ String getName() override;
+
+ void loopUpdate() override;
+
+ void onDisplayUpdate() override;
+
+ void onPotChanged(int potIndex, int prevValue, int newValue, int analogDelta) override;
+ void onKeyUpdate(OMXKeypadEvent e) override;
+ void drawLEDs() override;
+
+ protected:
+ void onEnabled() override;
+ void onDisabled() override;
+
+ void onEncoderChangedEditParam(Encoder::Update enc) override;
+
+ private:
+ bool m8mutesolo_[16];
+
+ // Control key mappings
+ uint8_t keyUp_ = 1;
+ uint8_t keyDown_ = 12;
+ uint8_t keyLeft_ = 11;
+ uint8_t keyRight_ = 13;
+
+ uint8_t but1_ = 3;
+ uint8_t but2_ = 14;
+ uint8_t but3_ = 15;
+
+ uint8_t enc1_ = 5;
+ uint8_t enc2_ = 16;
+ uint8_t enc3_ = 17;
+
+ uint8_t ccBut1_ = 85;
+ uint8_t ccBut2_ = 87;
+ uint8_t ccBut3_ = 88;
+
+ uint8_t ccEnc1_ = 58;
+ uint8_t ccEnc2_ = 62;
+ uint8_t ccEnc3_ = 63;
+ };
+}
diff --git a/Archive/OMX-27-firmware/src/modes/euclidean_sequencer.cpp b/Archive/OMX-27-firmware/src/modes/euclidean_sequencer.cpp
new file mode 100644
index 00000000..4af1db76
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/modes/euclidean_sequencer.cpp
@@ -0,0 +1,528 @@
+#include "euclidean_sequencer.h"
+#include "../midi/midi.h"
+#include "../midi/noteoffs.h"
+
+namespace euclidean
+{
+ // const float kEuclidNoteLengths[] = {0.1, 0.25, 0.5, 0.75, 1, 1.5, 2, 4, 8, 16};
+ // const uint8_t kNumEuclidNoteLengths = 10;
+
+ EuclideanMath::EuclideanMath()
+ {
+ }
+
+ // bool array should be of length kPatternSize
+ void EuclideanMath::generateEuclidPattern(bool *pattern, uint8_t events, uint8_t steps)
+ {
+ clearPattern(pattern);
+
+ // a value of true for each array element indicates a pulse
+
+ uint8_t bucket = 0; // out variable to add pulses together for each step
+
+ // fill array with rhythm
+ for (uint8_t i = 0; i < steps; i++)
+ {
+ bucket += events;
+ if (bucket >= steps)
+ {
+ bucket -= steps;
+ pattern[i] = true;
+ }
+ }
+
+ flipPattern(pattern, steps);
+ // rotatePattern(pattern, steps, rotation);
+ }
+ // bool array should be of length kPatternSize
+ void EuclideanMath::clearPattern(bool *pattern)
+ {
+ for (int i = 0; i < kPatternSize; i++)
+ {
+ pattern[i] = false;
+ }
+ }
+ // bool array should be of length kPatternSize
+ void EuclideanMath::flipPattern(bool *pattern, uint8_t steps)
+ {
+ bool temp[steps];
+
+ for (int i = 0; i < steps; i++)
+ {
+ temp[i] = pattern[steps - 1 - i];
+ }
+
+ for (int i = 0; i < steps; i++)
+ {
+ pattern[i] = temp[i];
+ }
+ }
+ // bool array should be of length kPatternSize
+ void EuclideanMath::rotatePattern(bool *pattern, uint8_t steps, uint8_t rotation)
+ {
+ bool temp[steps];
+
+ uint8_t val = steps - rotation;
+
+ for (uint8_t i = 0; i < steps; i++)
+ {
+ temp[i] = pattern[abs((i + val) % steps)];
+ }
+ for (int i = 0; i < steps; i++)
+ {
+ pattern[i] = temp[i];
+ }
+ }
+
+ EuclideanSequencer::EuclideanSequencer()
+ {
+ for (uint8_t i = 0; i < EuclideanMath::kPatternSize; i++)
+ {
+ pattern_[i] = false;
+ }
+
+ regeneratePattern();
+ tickCount_ = 0;
+
+ divider_ = 0;
+ multiplier_ = 1;
+ running_ = false;
+ }
+
+ void EuclideanSequencer::regeneratePattern()
+ {
+ EuclideanMath::generateEuclidPattern(pattern_, events_, steps_);
+ EuclideanMath::rotatePattern(pattern_, steps_, rotation_);
+
+ // printEuclidPattern();
+ }
+
+ uint32_t EuclideanSequencer::randomValue(uint32_t init)
+ {
+ uint32_t val = 0x12345;
+ if (init)
+ {
+ val = init;
+ return 0;
+ }
+ val = val * 214013 + 2531011;
+ return val;
+ }
+
+ void EuclideanSequencer::start()
+ {
+ tickCount_ = 0;
+ seqPos_ = 0;
+ running_ = true;
+
+ nextStepTimeP_ = seqConfig.currentFrameMicros;
+ lastStepTimeP_ = seqConfig.currentFrameMicros;
+ startMicros = seqConfig.currentFrameMicros;
+ }
+
+ void EuclideanSequencer::stop()
+ {
+ running_ = false;
+ triggered_ = false;
+ clockAdvanced_ = false;
+ pendingNoteOffs.allOff();
+ }
+
+ void EuclideanSequencer::proceed()
+ {
+ running_ = true;
+ }
+
+ bool EuclideanSequencer::isDirty()
+ {
+ return patternDirty_;
+ }
+
+ bool EuclideanSequencer::isRunning()
+ {
+ return running_;
+ }
+
+ void EuclideanSequencer::setNoteOutputFunc(void (*fptr)(void *, uint8_t, MidiNoteGroup), void *context, u_int8_t euclidIndex)
+ {
+ onNoteOnFuncPtr_ = fptr;
+ onNoteOnFuncPtrContext_ = context;
+ euclidIndex_ = euclidIndex;
+ }
+
+ void EuclideanSequencer::onNoteOn(uint8_t channel, uint8_t noteNumber, uint8_t velocity, float stepLength, bool sendMidi, bool sendCV, uint32_t noteOnMicros)
+ {
+ if (onNoteOnFuncPtrContext_ == nullptr)
+ return;
+
+ MidiNoteGroup noteGroup;
+ noteGroup.channel = channel;
+ noteGroup.noteNumber = noteNumber;
+ noteGroup.velocity = velocity;
+ noteGroup.stepLength = stepLength;
+ noteGroup.sendMidi = sendMidi;
+ noteGroup.sendCV = sendCV;
+ noteGroup.noteonMicros = noteOnMicros;
+
+ triggered_ = true;
+ triggerOffMicros_ = noteOnMicros + (stepLength * clockConfig.step_micros);
+
+ onNoteOnFuncPtr_(onNoteOnFuncPtrContext_, euclidIndex_, noteGroup);
+ }
+
+ void EuclideanSequencer::setMute(bool mute)
+ {
+ muted_ = mute;
+ }
+
+ bool EuclideanSequencer::getMute()
+ {
+ return muted_;
+ }
+
+ bool EuclideanSequencer::getTriggered()
+ {
+ return triggered_;
+ }
+
+ bool EuclideanSequencer::getClockAdvanced()
+ {
+ return clockAdvanced_;
+ }
+
+ void EuclideanSequencer::setClockDivMult(uint8_t m)
+ {
+ uint8_t prevDiv = clockDivMultP_;
+
+ clockDivMultP_ = m;
+ multiplier_ = multValues[m];
+
+ if (clockDivMultP_ != prevDiv)
+ {
+ // Serial.println((String)"clockDivMultP_: " + clockDivMultP_);
+ patternDirty_ = true;
+ }
+ }
+
+ uint8_t EuclideanSequencer::getClockDivMult()
+ {
+ return clockDivMultP_;
+ }
+
+ void EuclideanSequencer::setPolyRClockDivMult(uint8_t m)
+ {
+ uint8_t prevDiv = polyRClockDivMultP_;
+
+ polyRClockDivMultP_ = m;
+ multiplierPR_ = multValues[m];
+
+ if (polyRClockDivMultP_ != prevDiv)
+ {
+ patternDirty_ = true;
+ }
+ }
+ uint8_t EuclideanSequencer::getPolyRClockDivMult()
+ {
+ return polyRClockDivMultP_;
+ }
+
+ void EuclideanSequencer::setRotation(uint8_t newRotation)
+ {
+ if (newRotation != rotation_)
+ patternDirty_ = true;
+ rotation_ = newRotation;
+ }
+ uint8_t EuclideanSequencer::getRotation()
+ {
+ return rotation_;
+ }
+ void EuclideanSequencer::setEvents(uint8_t newEvents)
+ {
+ if (newEvents != events_)
+ patternDirty_ = true;
+ events_ = newEvents;
+ }
+ uint8_t EuclideanSequencer::getEvents()
+ {
+ return events_;
+ }
+
+ void EuclideanSequencer::setSteps(uint8_t newSteps)
+ {
+ if (newSteps != steps_)
+ patternDirty_ = true;
+ steps_ = newSteps;
+ }
+ uint8_t EuclideanSequencer::getSteps()
+ {
+ return steps_;
+ }
+ void EuclideanSequencer::setNoteNumber(uint8_t newNoteNumber)
+ {
+ noteNumber_ = newNoteNumber;
+ }
+ uint8_t EuclideanSequencer::getNoteNumber()
+ {
+ return noteNumber_;
+ }
+ void EuclideanSequencer::setMidiChannel(uint8_t newMidiChannel)
+ {
+ midiChannel_ = newMidiChannel;
+ }
+ uint8_t EuclideanSequencer::getMidiChannel()
+ {
+ return midiChannel_;
+ }
+
+ void EuclideanSequencer::setVelocity(uint8_t newVelocity)
+ {
+ velocity_ = newVelocity;
+ }
+ uint8_t EuclideanSequencer::getVelocity()
+ {
+ return velocity_;
+ }
+
+ void EuclideanSequencer::setSwing(uint8_t newSwing)
+ {
+ swing_ = newSwing;
+ }
+ uint8_t EuclideanSequencer::getSwing()
+ {
+ return swing_;
+ }
+
+ void EuclideanSequencer::setNoteLength(uint8_t newNoteLength)
+ {
+ noteLength_ = newNoteLength;
+ }
+ uint8_t EuclideanSequencer::getNoteLength()
+ {
+ return noteLength_;
+ }
+
+ void EuclideanSequencer::setPolyRhythmMode(bool enable)
+ {
+ polyRhythmMode_ = enable;
+ }
+ bool EuclideanSequencer::getPolyRhythmMode()
+ {
+ return polyRhythmMode_;
+ }
+
+ uint8_t EuclideanSequencer::getSeqPos()
+ {
+ return seqPos_;
+ }
+ uint8_t EuclideanSequencer::getLastSeqPos()
+ {
+ return lastSeqPos_;
+ }
+
+ float EuclideanSequencer::getSeqPerc()
+ {
+ return seqPerc_;
+ }
+
+ bool *EuclideanSequencer::getPattern()
+ {
+ return pattern_;
+ }
+
+ void EuclideanSequencer::printEuclidPattern()
+ {
+ String sOut = "";
+ for (uint8_t i = 0; i < steps_; i++)
+ {
+ sOut += (pattern_[i] ? "X" : "-");
+ }
+ Serial.println(sOut.c_str());
+ }
+ EuclidSave EuclideanSequencer::getSave()
+ {
+ EuclidSave save;
+
+ save.rotation_ = rotation_;
+ save.events_ = events_;
+ save.steps_ = steps_;
+ save.noteNumber_ = noteNumber_;
+ save.midiChannel_ = midiChannel_ - 1;
+ save.velocity_ = velocity_;
+ save.swing_ = swing_;
+ save.noteLength_ = noteLength_;
+ save.clockDivMultP_ = clockDivMultP_;
+ save.polyRClockDivMultP_ = polyRClockDivMultP_;
+ save.polyRhythmMode_ = polyRhythmMode_;
+ save.midifx = midiFXGroup;
+ save.muted = muted_;
+ return save;
+ }
+
+ void EuclideanSequencer::loadSave(EuclidSave save)
+ {
+ rotation_ = save.rotation_;
+ events_ = save.events_;
+ steps_ = save.steps_;
+ noteNumber_ = save.noteNumber_;
+ midiChannel_ = save.midiChannel_ + 1;
+ velocity_ = save.velocity_;
+ swing_ = save.swing_;
+ noteLength_ = save.noteLength_;
+ polyRhythmMode_ = save.polyRhythmMode_;
+ midiFXGroup = save.midifx;
+ muted_ = save.muted;
+
+ setClockDivMult(save.clockDivMultP_);
+ setPolyRClockDivMult(save.polyRClockDivMultP_);
+
+ patternDirty_ = true;
+
+ tickCount_ = 0;
+ seqPos_ = 0;
+
+ nextStepTimeP_ = micros();
+ lastStepTimeP_ = micros();
+ startMicros = micros();
+ }
+
+ void EuclideanSequencer::clockTick(uint32_t stepmicros, uint32_t microsperstep)
+ {
+ clockAdvanced_ = false;
+ if (patternDirty_)
+ {
+ regeneratePattern();
+ patternDirty_ = false;
+ }
+
+ if (!running_)
+ return;
+
+ if (triggered_)
+ {
+ if (stepmicros >= triggerOffMicros_)
+ {
+ triggered_ = false;
+ }
+ }
+
+ // seqPerc_ = (stepmicros - startMicros) / ((float)max(stepMicroDelta_, 1) * (steps_ + 1));
+
+ if (steps_ == 0)
+ {
+ seqPerc_ = 0;
+
+ return;
+ }
+
+ // uint32_t nextBarMicros = stepMicroDelta_ * (steps_ + 1);
+
+ if (stepmicros >= nextStepTimeP_)
+ {
+ lastStepTimeP_ = nextStepTimeP_;
+
+ clockAdvanced_ = true;
+
+ if (polyRhythmMode_) // Space all triggers across a bar
+ {
+ // stepMicroDelta_ = ((microsperstep * (16 * multiplierPR_)) / steps_) * multiplier_;
+ stepMicroDelta_ = ((microsperstep * 16) / steps_) * multiplierPR_;
+ // stepMicroDelta_ = ((microsperstep * (16 * multiplierPR_)) / steps_) * multiplier_;
+ }
+ else
+ {
+ stepMicroDelta_ = microsperstep * multiplier_;
+ }
+
+ nextStepTimeP_ += stepMicroDelta_; // calc step based on rate
+
+ bool trigger = pattern_[seqPos_];
+
+ if (trigger && !muted_)
+ {
+ playNote();
+ // pendingNoteOns.insert(60, 100, 1, stepmicros, false);
+ // Serial.print((String) "X ");
+ }
+ else
+ {
+ triggered_ = false;
+ // Serial.print((String) "- ");
+ }
+
+ // lastPosP_ = (seqPos_ + 15) % 16;
+
+ advanceStep(stepmicros);
+
+ if (seqPos_ == 0)
+ {
+
+ // Serial.print("\n\n\n");
+ }
+ }
+ }
+
+ void EuclideanSequencer::advanceStep(uint32_t stepmicros)
+ {
+
+ if (steps_ == 0)
+ {
+ seqPos_ = 0;
+ lastSeqPos_ = seqPos_;
+
+ return;
+ }
+ lastSeqPos_ = seqPos_;
+
+ seqPos_ = (seqPos_ + 1) % steps_;
+
+ if (seqPos_ == 0)
+ {
+ startMicros = stepmicros;
+ }
+ }
+
+ void EuclideanSequencer::autoReset()
+ {
+ }
+
+ void EuclideanSequencer::playNote()
+ {
+ bool sendnoteCV = false;
+ // if (sequencer.getPattern(patternNum)->sendCV) {
+ // sendnoteCV = true;
+ // }
+
+ // regular note on trigger
+ // uint8_t note = 60;
+ // uint8_t channel = 1;
+ // uint8_t vel = 100;
+ float stepLength = kNoteLengths[noteLength_];
+ // uint8_t swing = 0;
+
+ // uint32_t noteoff_micros = micros() + (stepLength) * clockConfig.step_micros;
+ // pendingNoteOffs.insert(noteNumber_, channel, noteoff_micros, sendnoteCV);
+
+ uint32_t noteon_micros = seqConfig.currentFrameMicros;
+
+ if (swing_ > 0 && seqPos_ % 2 == 0)
+ {
+ if (swing_ < 99)
+ {
+ noteon_micros = seqConfig.currentFrameMicros + ((clockConfig.ppqInterval * multiplier_) / (PPQ / 24) * swing_); // full range swing
+ }
+ else if (swing_ == 99)
+ { // random drunken swing
+ uint8_t rnd_swing = rand() % 95 + 1; // rand 1 - 95 // randomly apply swing value
+ noteon_micros = seqConfig.currentFrameMicros + ((clockConfig.ppqInterval * multiplier_) / (PPQ / 24) * rnd_swing);
+ }
+ }
+ else
+ {
+ // noteon_micros = micros();
+ }
+
+ // Queue note-on
+ onNoteOn(midiChannel_, noteNumber_, velocity_, stepLength, true, sendnoteCV, noteon_micros);
+ }
+
+}
diff --git a/Archive/OMX-27-firmware/src/modes/euclidean_sequencer.h b/Archive/OMX-27-firmware/src/modes/euclidean_sequencer.h
new file mode 100644
index 00000000..c53bb50c
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/modes/euclidean_sequencer.h
@@ -0,0 +1,262 @@
+#pragma once
+
+#include
+#include "../config.h"
+// #define NUM_GRIDS 8
+
+namespace euclidean
+{
+ // extern const float kEuclidNoteLengths[10];
+ // extern const uint8_t kNumEuclidNoteLengths;
+
+ struct EuclidSave
+ {
+ uint8_t rotation_ : 6;
+ uint8_t events_ : 6;
+ uint8_t steps_ : 6;
+
+ uint8_t noteNumber_ : 7;
+ uint8_t midiChannel_ : 4;
+ uint8_t velocity_ : 7;
+ uint8_t swing_ : 7;
+
+ uint8_t noteLength_ : 4;
+ int8_t midifx : 4;
+
+ bool muted = false;
+
+ bool polyRhythmMode_ = true;
+
+ uint8_t clockDivMultP_ : 3;
+ uint8_t polyRClockDivMultP_ : 3;
+
+ EuclidSave()
+ {
+ rotation_ = 0;
+ events_ = 0;
+ steps_ = 0;
+
+ noteNumber_ = 60;
+ midiChannel_ = 0;
+ velocity_ = 100;
+ swing_ = 0;
+ noteLength_ = 1;
+ midifx = 0;
+ muted = false;
+ polyRhythmMode_ = false;
+ clockDivMultP_ = 4;
+ polyRClockDivMultP_ = 4;
+ }
+ };
+
+ // #define EUCLID_PAT_SIZE = 32
+ // enum Grid_Resolutions
+ // {
+ // HALF = 0,
+ // NORMAL,
+ // DOUBLE,
+ // FOUR,
+ // COUNT
+ // };
+
+ // struct InstSettings
+ // {
+ // uint8_t note = 60;
+ // uint8_t midiChan = 1;
+ // uint8_t density = 0;
+ // uint8_t x = 128;
+ // uint8_t y = 128;
+ // };
+
+ // struct SnapShotSettings
+ // {
+ // InstSettings instruments[4];
+ // uint8_t chaos = 0;
+ // uint8_t accent = 128;
+ // uint8_t resolution = 1;
+ // };
+
+ // constexpr uint8_t kStepsPerPattern = 32;
+
+ // struct ChannelPatternLEDs
+ // {
+ // uint8_t levels[kStepsPerPattern];
+ // };
+
+ class EuclideanMath
+ {
+ public:
+ static const uint8_t kPatternSize = 32; // All pattern arrays are 32 length
+ EuclideanMath();
+
+ // bool array should be of length kPatternSize
+ static void generateEuclidPattern(bool *pattern, uint8_t events, uint8_t steps);
+ // bool array should be of length kPatternSize
+ static void clearPattern(bool *pattern);
+ // bool array should be of length kPatternSize
+ static void flipPattern(bool *pattern, uint8_t steps);
+ // bool array should be of length kPatternSize
+ static void rotatePattern(bool *pattern, uint8_t steps, uint8_t rotation);
+ };
+
+ class EuclideanSequencer
+ {
+ public:
+ // uint8_t grids_notes[4] = {36, 38, 42, 46};
+ // static const uint8_t num_notes = sizeof(grids_notes);
+ // uint8_t playingPattern = 0;
+
+ // static const uint8_t kStepsPerPattern = 16;
+
+ uint8_t midiFXGroup = 0;
+
+ // SnapShotSettings snapshots[8];
+
+ EuclideanSequencer();
+
+ void start();
+ void stop();
+ void proceed();
+ void clockTick(uint32_t stepmicros, uint32_t microsperstep);
+
+ // void saveSnapShot(uint8_t snapShotIndex);
+ // void loadSnapShot(uint8_t snapShotIndex);
+ // SnapShotSettings* getSnapShot(uint8_t snapShotIndex);
+ // void setSnapShot(uint8_t snapShotIndex, SnapShotSettings snapShot);
+
+ static uint32_t randomValue(uint32_t init = 0);
+
+ // ChannelPatternLEDs getChannelLEDS(uint8_t channel);
+
+ // uint8_t getSeqPos();
+
+ // bool getChannelTriggered(uint8_t chanIndex);
+
+ // void setMidiChan(uint8_t chanIndex, uint8_t channel);
+ // uint8_t getMidiChan(uint8_t chanIndex);
+
+ bool isDirty();
+
+ bool isRunning();
+
+ void setNoteOutputFunc(void (*fptr)(void *, uint8_t, MidiNoteGroup), void *context, u_int8_t euclidIndex);
+
+ void setMute(bool mute);
+ bool getMute();
+
+ bool getTriggered();
+ bool getClockAdvanced();
+
+ void setClockDivMult(uint8_t m);
+ uint8_t getClockDivMult();
+
+ void setPolyRClockDivMult(uint8_t m);
+ uint8_t getPolyRClockDivMult();
+
+ void setRotation(uint8_t newRotation);
+ uint8_t getRotation();
+
+ void setEvents(uint8_t newEvents);
+ uint8_t getEvents();
+
+ void setSteps(uint8_t newSteps);
+ uint8_t getSteps();
+
+ void setNoteNumber(uint8_t newNoteNumber);
+ uint8_t getNoteNumber();
+
+ void setMidiChannel(uint8_t newMidiChannel);
+ uint8_t getMidiChannel();
+
+ void setVelocity(uint8_t newVelocity);
+ uint8_t getVelocity();
+
+ void setSwing(uint8_t newSwing);
+ uint8_t getSwing();
+
+ void setNoteLength(uint8_t newNoteLength);
+ uint8_t getNoteLength();
+
+ void setPolyRhythmMode(bool enable);
+ bool getPolyRhythmMode();
+
+ uint8_t getSeqPos();
+ uint8_t getLastSeqPos();
+
+ float getSeqPerc();
+
+ bool *getPattern();
+
+ void printEuclidPattern();
+
+ EuclidSave getSave();
+ void loadSave(EuclidSave save);
+
+ private:
+ // GridsChannel channel_;
+ uint32_t divider_;
+ float multiplier_ = 1;
+ float multiplierPR_ = 1;
+ uint32_t tickCount_;
+ // uint8_t density_[num_notes];
+ // uint8_t perturbations_[num_notes];
+ // uint8_t x_[num_notes];
+ // uint8_t y_[num_notes];
+ // uint8_t midiChannels_[num_notes];
+ // bool channelTriggered_[num_notes];
+ // uint8_t triggeredNotes_[num_notes]; // Keep track of triggered notes to avoid stuck notes
+ // uint8_t resolution_;
+ bool running_;
+ bool muted_ = false;
+
+ // Note On pointers
+ uint8_t euclidIndex_;
+ void *onNoteOnFuncPtrContext_;
+ void (*onNoteOnFuncPtr_)(void *, uint8_t, MidiNoteGroup);
+ void onNoteOn(uint8_t channel, uint8_t noteNumber, uint8_t velocity, float stepLength, bool sendMidi, bool sendCV, uint32_t noteOnMicros);
+
+ // uint8_t defaultMidiChannel_ = 1;
+
+ uint8_t rotation_ = 0;
+ uint8_t events_ = 0;
+ uint8_t steps_ = 0;
+
+ uint8_t noteNumber_ = 16;
+ uint8_t midiChannel_ = 1;
+ uint8_t velocity_ = 100;
+ uint8_t swing_ = 0;
+
+ uint8_t noteLength_ = 1;
+
+ bool polyRhythmMode_ = true;
+
+ bool patternDirty_ = false;
+
+ bool triggered_ = false;
+ bool clockAdvanced_ = false;
+
+ // Clock timings
+ Micros lastProcessTimeP_ = 32;
+ Micros nextStepTimeP_ = 32;
+ Micros lastStepTimeP_ = 32;
+ uint8_t lastPosP_ = 16;
+ uint8_t clockDivMultP_ = 4;
+ uint8_t polyRClockDivMultP_ = 4;
+
+ uint8_t seqPos_ = 0;
+ uint8_t lastSeqPos_ = 0;
+
+ float seqPerc_ = 0;
+ uint32_t stepMicroDelta_ = 0;
+ uint32_t startMicros = 0;
+ uint32_t triggerOffMicros_ = 0;
+
+ bool pattern_[EuclideanMath::kPatternSize];
+ void regeneratePattern();
+
+ void advanceStep(uint32_t stepmicros);
+ void autoReset();
+ void playNote();
+ };
+
+}
diff --git a/Archive/OMX-27-firmware/src/modes/omx_mode_chords.cpp b/Archive/OMX-27-firmware/src/modes/omx_mode_chords.cpp
new file mode 100644
index 00000000..9c2626b9
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/modes/omx_mode_chords.cpp
@@ -0,0 +1,3561 @@
+#include "omx_mode_chords.h"
+#include "../config.h"
+#include "../consts/colors.h"
+#include "../utils/omx_util.h"
+#include "../utils/cvNote_util.h"
+#include "../hardware/omx_disp.h"
+#include "../hardware/omx_leds.h"
+// #include "../sequencer.h"
+#include "../midi/midi.h"
+#include "../midi/noteoffs.h"
+
+
+enum ChordsModePage
+{
+ CHRDPAGE_NOTES,
+ CHRDPAGE_GBL1, // UI Mode
+ // CHRDPAGE_GBL2, // Manual Strum, M-Chan,
+ CHRDPAGE_OUTMIDI, // Oct, CH, Vel
+ CHRDPAGE_POTSANDMACROS, // PotBank, Thru, Macro, Macro Channel
+ CHRDPAGE_SCALES, // Root, Scale, Lock Scale Notes, Group notes.
+ CHRDPAGE_1, // Chord Type, MidiFX, 0, Midi Channel
+ CHRDPAGE_2, // Note, Octave, Chord, | numNotes, degree, octave, transpose
+ CHRDPAGE_3, // | spread, rotate, voicing
+ CHRDPAGE_4, // | spreadUpDown, quartalVoicing
+};
+
+
+
+// enum ChordsModeIntervalPage {
+// CHRDINTPAGE_NOTES,
+// CHRDINTPAGE_GBL1, // Root, Scale, Octave
+// CHRDINTPAGE_1, // numNotes, degree, octave, transpose
+// CHRDINTPAGE_2, // spread, rotate, voicing
+// CHRDINTPAGE_3, // spreadUpDown, quartalVoicing
+// };
+
+// enum ChordsModeBasicPage {
+// CHRDBASPAGE_NOTES,
+// CHRDBASPAGE_GBL1, // Root, Scale, Octave
+// CHRDBASPAGE_1, // numNotes, degree, octave, transpose
+// CHRDBASPAGE_2, // spread, rotate, voicing
+// CHRDBASPAGE_3, // spreadUpDown, quartalVoicing
+// };
+
+enum ChordsUIModes
+{
+ CUIMODE_FULL,
+ CUIMODE_SPLIT
+};
+
+const char *kUIModeDisp[2] = {"FULL", "SPLT"};
+
+enum ChordsMainMode
+{
+ CHRDMODE_PLAY, // Play Chords
+ CHRDMODE_EDIT, // Play Chords, jumps to edit page
+ // CHRDMODE_PRESET, // Replaced by preset manager // Loads groups of chord presets
+ CHRDMODE_MANSTRUM, // Manually strum chords using the encoder
+};
+
+
+
+// const int chordPatterns[16][3] = {
+// { -1, -1, -1 }, // 0: N/A
+// { 4, 7, -1 }, // 1: MAJ
+// { 3, 7, -1 }, // 2: MIN
+// { 4, 7, 11 }, // 3: MAJ7
+// { 3, 7, 10 }, // 4: MIN7
+// { 4, 7, 10 }, // 5: 7
+// { 2, 7, -1 }, // 6: SUS2
+// { 5, 7, -1 }, // 7: SUS4
+// { 4, 8, -1 }, // 8: AUG
+// { 3, 6, -1 }, // 9: DIM
+// { 3, 6, 10 }, // 10: HDIM
+// { 7, -1, -1 }, // 11: 5
+// { 4, 11, 14 }, // 12: MAJ9
+// { 3, 10, 14 }, // 13: MIN9
+// { 4, 10, 14 }, // 14: 9
+// { 5, 7, 11 }, // 15: 7SUS4
+// };
+
+// minor
+// major
+// sus2
+// sus4
+// m7
+// M7
+// hMaj7
+// Maj7
+// 7sus4
+// dim7
+// madd9 or hadd9
+// Madd9
+// m6
+// M6
+// mb5
+// Mb5
+// m7b5
+// M7b5
+// M#5
+// m7#5
+// M7#5
+// mb6
+// m9nos
+// M9nos
+// Madd9b5
+// Maj7b5
+// M7b9nos
+// sus4#5b9
+// sus4add#5
+// Maddb5
+// M6add4nos
+// Maj7/6nos
+// Maj9nos
+// Fourths
+// Fifths
+// C C# D D# E F F# G G# A A# B C C# D D#
+// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
+
+const int kDegreeColor = ORANGE;
+const int kDegreeSelColor = 0xFFBF80;
+const int kNumNotesColor = BLUE;
+const int kNumNotesSelColor = 0x9C9CFF;
+const int kSpreadUpDownOnColor = RED;
+const int kSpreadUpDownOffColor = 0x550000;
+const int kQuartalVoicingOnColor = MAGENTA;
+const int kQuartalVoicingOffColor = 0x500050;
+const int kOctaveColor = BLUE;
+const int kTransposeColor = BLUE;
+const int kSpreadColor = BLUE;
+const int kRotateColor = BLUE;
+const int kVoicingColor = BLUE;
+
+const int kPlayColor = ORANGE;
+const int kEditColor = DKRED;
+const int kPresetColor = DKGREEN;
+
+const int kChordEditNoteInScaleColor = 0x040404;
+// const int kChordEditNoteRootColor = MAGENTA;
+// const int kChordEditNoteChordColor = ORANGE;
+
+// const uint16_t kChordEditNoteRootHue = MAGENTA;
+const uint16_t kChordEditNoteChordHue = 5461; // Orange
+
+OmxModeChords::OmxModeChords()
+{
+// enum ChordsModePage
+// {
+// CHRDPAGE_NOTES,
+// CHRDPAGE_GBL1, // UI Mode
+// // CHRDPAGE_GBL2, // Manual Strum, M-Chan,
+// CHRDPAGE_OUTMIDI, // Oct, CH, Vel
+// CHRDPAGE_POTSANDMACROS, // PotBank, Thru, Macro, Macro Channel
+// CHRDPAGE_SCALES, // Root, Scale, Lock Scale Notes, Group notes.
+// CHRDPAGE_1, // Chord Type, MidiFX, 0, Midi Channel
+// CHRDPAGE_2, // Note, Octave, Chord, | numNotes, degree, octave, transpose
+// CHRDPAGE_3, // | spread, rotate, voicing
+// CHRDPAGE_4, // | spreadUpDown, quartalVoicing
+// };
+
+ basicParams_.addPage(1);
+ basicParams_.addPage(4);
+ basicParams_.addPage(4);
+ basicParams_.addPage(4);
+ basicParams_.addPage(4);
+ basicParams_.addPage(4);
+ basicParams_.addPage(4);
+ basicParams_.addPage(6); // Custom chord notes
+
+ intervalParams_.addPage(1);
+ intervalParams_.addPage(4);
+ intervalParams_.addPage(4);
+ intervalParams_.addPage(4);
+ intervalParams_.addPage(4);
+ intervalParams_.addPage(4);
+ intervalParams_.addPage(4);
+ intervalParams_.addPage(4);
+ intervalParams_.addPage(4);
+
+ // 808 Colors
+ for (uint8_t i = 0; i < 16; i++)
+ {
+ if (i >= 0 && i < 4)
+ {
+ chords_[i].color = RED; // Red
+ }
+ else if (i >= 4 && i < 8)
+ {
+ chords_[i].color = ORANGE; // Orange
+ }
+ else if (i >= 8 && i < 12)
+ {
+ chords_[i].color = YELLOW; // Yellow
+ }
+ else if (i >= 12)
+ {
+ chords_[i].color = 0xcfc08f; // Creme
+ }
+ }
+
+ for (uint8_t i = 0; i < 16; i++)
+ {
+ chords_[i].type = CTYPE_BASIC;
+ chords_[i].chord = i <= 7 ? 0 : 1; // Major left, minor right
+ chords_[i].balance = 40;
+
+ int adjnote = notes[i + 11] + (midiSettings.octave * 12);
+
+ if (adjnote >= 0 && adjnote <= 127)
+ {
+ chords_[i].note = adjnote % 12;
+ chords_[i].basicOct = (adjnote / 12) - 5;
+ }
+ }
+
+ // save these to presets
+ for (uint8_t i = 0; i < NUM_CHORD_SAVES; i++)
+ {
+ savePreset(i);
+ }
+
+ activeChordEditDegree_ = -1;
+ activeChordEditNoteKey_ = -1;
+
+ uiMode_ = CUIMODE_SPLIT;
+
+ m8Macro_.setDoNoteOn(&OmxModeChords::doNoteOnForwarder, this);
+ m8Macro_.setDoNoteOff(&OmxModeChords::doNoteOffForwarder, this);
+ nornsMarco_.setDoNoteOn(&OmxModeChords::doNoteOnForwarder, this);
+ nornsMarco_.setDoNoteOff(&OmxModeChords::doNoteOffForwarder, this);
+ delugeMacro_.setDoNoteOn(&OmxModeChords::doNoteOnForwarder, this);
+ delugeMacro_.setDoNoteOff(&OmxModeChords::doNoteOffForwarder, this);
+
+ presetManager.setContextPtr(this);
+ presetManager.setDoSaveFunc(&OmxModeChords::doSavePresetForwarder);
+ presetManager.setDoLoadFunc(&OmxModeChords::doLoadPresetForwarder);
+
+ // chords_[0].numNotes = 3;
+ // chords_[0].degree = 0;
+
+ // chords_[1].numNotes = 3;
+ // chords_[1].degree = 1;
+
+ // chords_[2].numNotes = 4;
+ // chords_[2].degree = 0;
+
+ // chords_[3].numNotes = 4;
+ // chords_[3].degree = 1;
+}
+
+midimacro::MidiMacroInterface *OmxModeChords::getActiveMacro()
+{
+ switch (midiMacroConfig.midiMacro)
+ {
+ case 1:
+ return &m8Macro_;
+ case 2:
+ return &nornsMarco_;
+ case 3:
+ return &delugeMacro_;
+ }
+ return nullptr;
+}
+
+void OmxModeChords::InitSetup()
+{
+}
+
+void OmxModeChords::onModeActivated()
+{
+ basicParams_.setSelPageAndParam(0, 0);
+ intervalParams_.setSelPageAndParam(0, 0);
+
+ encoderSelect_ = true;
+ heldChord_ = -1;
+ activeChordEditDegree_ = -1;
+ activeChordEditNoteKey_ = -1;
+
+ lockScaleCache_ = scaleConfig.lockScale;
+ grp16ScaleCache_ = scaleConfig.group16;
+
+ scaleConfig.lockScale = false;
+ scaleConfig.group16 = false;
+
+ // sequencer.playing = false;
+ stopSequencers();
+
+ omxLeds.setDirty();
+ omxDisp.setDirty();
+
+ for (uint8_t i = 0; i < NUM_MIDIFX_GROUPS; i++)
+ {
+ subModeMidiFx[i].setEnabled(true);
+ subModeMidiFx[i].setSelected(true);
+ subModeMidiFx[i].onModeChanged();
+ subModeMidiFx[i].setNoteOutputFunc(&OmxModeChords::onNotePostFXForwarder, this);
+ }
+
+ pendingNoteOffs.setNoteOffFunction(&OmxModeChords::onPendingNoteOffForwarder, this);
+
+ selectMidiFx(mfxIndex_, false);
+}
+
+void OmxModeChords::onModeDeactivated()
+{
+ // sequencer.playing = false;
+ stopSequencers();
+ allNotesOff();
+
+ for (uint8_t i = 0; i < NUM_MIDIFX_GROUPS; i++)
+ {
+ subModeMidiFx[i].setEnabled(false);
+ subModeMidiFx[i].onModeChanged();
+ }
+
+ scaleConfig.lockScale = lockScaleCache_;
+ scaleConfig.group16 = grp16ScaleCache_;
+}
+
+void OmxModeChords::stopSequencers()
+{
+ omxUtil.stopClocks();
+ // MM::stopClock();
+ pendingNoteOffs.allOff();
+}
+
+void OmxModeChords::selectMidiFx(uint8_t mfxIndex, bool dispMsg)
+{
+ this->mfxIndex_ = mfxIndex;
+
+ if(mfxQuickEdit_)
+ {
+ // Change the MidiFX Group being edited
+ if(mfxIndex < NUM_MIDIFX_GROUPS && mfxIndex != quickEditMfxIndex_)
+ {
+ enableSubmode(&subModeMidiFx[mfxIndex]);
+ subModeMidiFx[mfxIndex].enablePassthrough();
+ quickEditMfxIndex_ = mfxIndex;
+ dispMsg = false;
+ }
+ else if(mfxIndex >= NUM_MIDIFX_GROUPS)
+ {
+ disableSubmode();
+ }
+ }
+
+ for (uint8_t i = 0; i < NUM_MIDIFX_GROUPS; i++)
+ {
+ subModeMidiFx[i].setSelected(true);
+ }
+
+ if (dispMsg)
+ {
+ if (mfxIndex < NUM_MIDIFX_GROUPS)
+ {
+ omxDisp.displayMessageTimed("Key MFX " + String(mfxIndex + 1), 5);
+ }
+ else
+ {
+ omxDisp.displayMessageTimed("Key MFX Off", 5);
+ }
+ }
+}
+
+void OmxModeChords::selectMidiFxChordKey(int8_t mfxIndex, bool dispMsg)
+{
+ int8_t prevMidiFX = chords_[selectedChord_].midiFx;
+
+ if(mfxIndex != prevMidiFX && (prevMidiFX >= 0 && prevMidiFX < NUM_MIDIFX_GROUPS))
+ {
+ onChordOff(selectedChord_);
+ }
+
+ chords_[selectedChord_].midiFx = mfxIndex;
+
+ if(mfxQuickEdit_)
+ {
+ // Change the MidiFX Group being edited
+ if(mfxIndex < NUM_MIDIFX_GROUPS && mfxIndex != quickEditMfxIndex_)
+ {
+ enableSubmode(&subModeMidiFx[mfxIndex]);
+ subModeMidiFx[mfxIndex].enablePassthrough();
+ quickEditMfxIndex_ = mfxIndex;
+ dispMsg = false;
+ }
+ else if(mfxIndex >= NUM_MIDIFX_GROUPS)
+ {
+ disableSubmode();
+ }
+ }
+
+ for (uint8_t i = 0; i < NUM_MIDIFX_GROUPS; i++)
+ {
+ subModeMidiFx[i].setSelected(i == mfxIndex);
+ }
+
+ if (dispMsg)
+ {
+ if (mfxIndex >= 0 && mfxIndex < NUM_MIDIFX_GROUPS)
+ {
+ omxDisp.displayMessageTimed("Chord MFX " + String(mfxIndex + 1), 5);
+ }
+ else
+ {
+ omxDisp.displayMessageTimed("Chord MFX Off", 5);
+ }
+ }
+}
+
+void OmxModeChords::onClockTick()
+{
+ for (uint8_t i = 0; i < NUM_MIDIFX_GROUPS; i++)
+ {
+ // Lets them do things in background
+ subModeMidiFx[i].onClockTick();
+ }
+}
+
+void OmxModeChords::onPotChanged(int potIndex, int prevValue, int newValue, int analogDelta)
+{
+ if (isSubmodeEnabled() && activeSubmode->usesPots())
+ {
+ activeSubmode->onPotChanged(potIndex, prevValue, newValue, analogDelta);
+ return;
+ }
+
+ auto activeMacro = getActiveMacro();
+
+ bool macroConsumesPots = false;
+ if (activeMacro != nullptr)
+ {
+ macroConsumesPots = activeMacro->consumesPots();
+ }
+
+ if (macroActive_ && macroConsumesPots)
+ {
+ activeMacro->onPotChanged(potIndex, prevValue, newValue, analogDelta);
+ omxDisp.setDirty();
+ return;
+ }
+
+ // Serial.println("onPotChanged: " + String(potIndex));
+ if (chordEditMode_ == false && mode_ == CHRDMODE_MANSTRUM)
+ {
+ if (analogDelta < 3)
+ {
+ return;
+ }
+
+ // Serial.println("strum");
+
+ if (potIndex == 0)
+ {
+ uint8_t oldV = manStrumSensit_;
+ manStrumSensit_ = (uint8_t)map(newValue, 0, 127, 1, 32);
+ if (manStrumSensit_ != oldV)
+ {
+ omxDisp.displayMessageTimed("Sens: " + String(manStrumSensit_), 5);
+ }
+ }
+ else if (potIndex == 1)
+ {
+ bool oldV = wrapManStrum_;
+ wrapManStrum_ = (bool)map(newValue, 0, 127, 0, 1);
+ if (wrapManStrum_ != oldV)
+ {
+ if (wrapManStrum_)
+ {
+ omxDisp.displayMessageTimed("Wrap on", 5);
+ }
+ else
+ {
+ omxDisp.displayMessageTimed("Wrap off", 5);
+ }
+ }
+ }
+ else if (potIndex == 2)
+ {
+ uint8_t oldV = incrementManStrum_;
+ incrementManStrum_ = (uint8_t)map(newValue, 0, 127, 0, 4);
+ if (incrementManStrum_ != oldV)
+ {
+ omxDisp.displayMessageTimed("Increm: " + String(incrementManStrum_), 5);
+ }
+ }
+ else if (potIndex == 3)
+ {
+ // Serial.println("length");
+
+ uint8_t prevLength = manStrumNoteLength_;
+ manStrumNoteLength_ = map(newValue, 0, 127, 0, kNumNoteLengths - 1);
+
+ if (prevLength != manStrumNoteLength_)
+ {
+ omxDisp.displayMessageTimed(String(kNoteLengths[manStrumNoteLength_]), 10);
+ }
+ }
+
+ omxDisp.setDirty();
+ omxLeds.setDirty();
+ return;
+ }
+ else
+ {
+ omxUtil.sendPots(potIndex, sysSettings.midiChannel);
+ omxDisp.setDirty();
+ }
+}
+
+void OmxModeChords::loopUpdate(Micros elapsedTime)
+{
+ updateFuncKeyMode();
+
+ for (uint8_t i = 0; i < 5; i++)
+ {
+ // Lets them do things in background
+ subModeMidiFx[i].loopUpdate();
+ }
+
+ // Can be modified by scale MidiFX
+ musicScale_->calculateScaleIfModified(scaleConfig.scaleRoot, scaleConfig.scalePattern);
+}
+
+void OmxModeChords::allNotesOff()
+{
+ omxUtil.allOff();
+}
+
+void OmxModeChords::updateFuncKeyMode()
+{
+ auto keyState = midiSettings.keyState;
+
+ uint8_t prevMode = funcKeyMode_;
+
+ funcKeyMode_ = FUNCKEYMODE_NONE;
+
+ if (!auxDown_)
+ {
+ if (keyState[1] && !keyState[2])
+ {
+ funcKeyMode_ = FUNCKEYMODE_F1;
+ }
+ else if (!keyState[1] && keyState[2])
+ {
+ funcKeyMode_ = FUNCKEYMODE_F2;
+ }
+ else if (keyState[1] && keyState[2])
+ {
+ funcKeyMode_ = FUNCKEYMODE_F3;
+ }
+ else
+ {
+ funcKeyMode_ = FUNCKEYMODE_NONE;
+ }
+ }
+
+ if (funcKeyMode_ != prevMode)
+ {
+ // omxUtil.allOff();
+
+ omxDisp.setDirty();
+ omxLeds.setDirty();
+ }
+}
+
+void OmxModeChords::onEncoderChanged(Encoder::Update enc)
+{
+ if (isSubmodeEnabled())
+ {
+ activeSubmode->onEncoderChanged(enc);
+ return;
+ }
+
+ bool macroConsumesDisplay = false;
+
+ if (macroActive_ && activeMacro_ != nullptr)
+ {
+ macroConsumesDisplay = activeMacro_->consumesDisplay();
+ }
+
+ if (macroConsumesDisplay)
+ {
+ activeMacro_->onEncoderChanged(enc);
+ return;
+ }
+
+ if (chordEditMode_ == false && mode_ == CHRDMODE_MANSTRUM)
+ {
+ onEncoderChangedManStrum(enc);
+ return;
+ }
+
+ auto params = getParams();
+
+ if (getEncoderSelect())
+ {
+ params->changeParam(enc.dir());
+ omxDisp.setDirty();
+ return;
+ }
+
+ int8_t selPage = params->getSelPage();
+ int8_t selParam = params->getSelParam() + 1; // Add one for readability
+
+ // Global 1 - UI Mode, Root, Scale, Octave
+ if (selPage == CHRDPAGE_GBL1)
+ {
+ onEncoderChangedEditParam(&enc, selParam, 1, CPARAM_UIMODE);
+ // onEncoderChangedEditParam(&enc, selParam, 2, CPARAM_SCALE_ROOT);
+ // onEncoderChangedEditParam(&enc, selParam, 3, CPARAM_SCALE_PAT);
+ // onEncoderChangedEditParam(&enc, selParam, 4, CPARAM_GBL_OCT);
+ }
+ else if (selPage == CHRDPAGE_OUTMIDI)
+ {
+ omxUtil.onEncoderChangedEditParam(&enc, selParam, 1, GPARAM_MOUT_OCT);
+ omxUtil.onEncoderChangedEditParam(&enc, selParam, 2, GPARAM_MOUT_CHAN);
+ omxUtil.onEncoderChangedEditParam(&enc, selParam, 3, GPARAM_MOUT_VEL);
+
+ // onEncoderChangedEditParam(&enc, selParam, 1, CPARAM_GBL_OCT);
+ // onEncoderChangedEditParam(&enc, selParam, 2, CPARAM_GBL_MCHAN);
+ // onEncoderChangedEditParam(&enc, selParam, 3, CPARAM_GBL_VEL);
+ }
+ else if (selPage == CHRDPAGE_POTSANDMACROS)
+ {
+ omxUtil.onEncoderChangedEditParam(&enc, selParam, 1, GPARAM_POTS_PBANK);
+ omxUtil.onEncoderChangedEditParam(&enc, selParam, 2, GPARAM_MIDI_THRU);
+ omxUtil.onEncoderChangedEditParam(&enc, selParam, 3, GPARAM_MACRO_MODE);
+ omxUtil.onEncoderChangedEditParam(&enc, selParam, 4, GPARAM_MACRO_CHAN);
+
+ // onEncoderChangedEditParam(&enc, selParam, 1, CPARAM_GBL_PBANK);
+ // onEncoderChangedEditParam(&enc, selParam, 2, CPARAM_GBL_MIDITHRU);
+ // onEncoderChangedEditParam(&enc, selParam, 3, CPARAM_GBL_MIDIMACRO);
+ // onEncoderChangedEditParam(&enc, selParam, 4, CPARAM_GBL_MACROCHAN);
+ }
+ else if (selPage == CHRDPAGE_SCALES)
+ {
+ omxUtil.onEncoderChangedEditParam(&enc, musicScale_, selParam, 1, GPARAM_SCALE_ROOT);
+ omxUtil.onEncoderChangedEditParam(&enc, musicScale_, selParam, 2, GPARAM_SCALE_PAT);
+ omxUtil.onEncoderChangedEditParam(&enc, musicScale_, selParam, 3, GPARAM_SCALE_LOCK);
+ omxUtil.onEncoderChangedEditParam(&enc, musicScale_, selParam, 4, GPARAM_SCALE_GRP16);
+
+ // onEncoderChangedEditParam(&enc, selParam, 1, CPARAM_SCALE_ROOT);
+ // onEncoderChangedEditParam(&enc, selParam, 2, CPARAM_SCALE_PAT);
+ // onEncoderChangedEditParam(&enc, selParam, 3, CPARAM_SCALE_LOCK);
+ // onEncoderChangedEditParam(&enc, selParam, 4, CPARAM_SCALE_GRP16);
+ }
+ // else if (selPage == CHRDPAGE_GBL2)
+ // {
+ // onEncoderChangedEditParam(&enc, selParam, 1, CPARAM_MAN_STRUM);
+ // onEncoderChangedEditParam(&enc, selParam, 2, CPARAM_GBL_MCHAN);
+ // onEncoderChangedEditParam(&enc, selParam, 4, CPARAM_GBL_MCHAN);
+ // }
+ // PAGE ONE - Chord Type, MidiFX, 0, Midi Channel
+ else if (selPage == CHRDPAGE_1)
+ {
+ onEncoderChangedEditParam(&enc, selParam, 1, CPARAM_CHORD_TYPE);
+ onEncoderChangedEditParam(&enc, selParam, 2, CPARAM_CHORD_MFX);
+ onEncoderChangedEditParam(&enc, selParam, 3, CPARAM_CHORD_VEL);
+ onEncoderChangedEditParam(&enc, selParam, 4, CPARAM_CHORD_MCHAN);
+ }
+ // PAGE TWO - Basic: Note, Octave, Chord Interval: numNotes, degree, octave, transpose
+ else if (selPage == CHRDPAGE_2)
+ {
+ if (chords_[selectedChord_].type == CTYPE_INTERVAL)
+ {
+ onEncoderChangedEditParam(&enc, selParam, 1, CPARAM_INT_NUMNOTES);
+ onEncoderChangedEditParam(&enc, selParam, 2, CPARAM_INT_DEGREE);
+ onEncoderChangedEditParam(&enc, selParam, 3, CPARAM_INT_OCTAVE);
+ onEncoderChangedEditParam(&enc, selParam, 4, CPARAM_INT_TRANSPOSE);
+ }
+ else if (chords_[selectedChord_].type == CTYPE_BASIC)
+ {
+ onEncoderChangedEditParam(&enc, selParam, 1, CPARAM_BAS_NOTE);
+ onEncoderChangedEditParam(&enc, selParam, 2, CPARAM_BAS_OCT);
+ onEncoderChangedEditParam(&enc, selParam, 3, CPARAM_BAS_BALANCE);
+ onEncoderChangedEditParam(&enc, selParam, 4, CPARAM_BAS_CHORD);
+ }
+ }
+ // PAGE THREE - spread, rotate, voicing
+ else if (selPage == CHRDPAGE_3)
+ {
+ if (chords_[selectedChord_].type == CTYPE_INTERVAL)
+ {
+ onEncoderChangedEditParam(&enc, selParam, 1, CPARAM_INT_SPREAD);
+ onEncoderChangedEditParam(&enc, selParam, 2, CPARAM_INT_ROTATE);
+ onEncoderChangedEditParam(&enc, selParam, 3, CPARAM_INT_VOICING);
+ }
+ else if (chords_[selectedChord_].type == CTYPE_BASIC)
+ {
+ auto amtSlow = enc.accel(1);
+ int8_t sel = params->getSelParam();
+ chords_[selectedChord_].customNotes[sel].note = constrain(chords_[selectedChord_].customNotes[sel].note + amtSlow, -48, 48);
+
+ if (amtSlow != 0) // To see notes change on keyboard leds
+ {
+ constructChord(selectedChord_);
+ }
+ }
+ }
+ // PAGE FOUR - spreadUpDown, quartalVoicing
+ else if (selPage == CHRDPAGE_4)
+ {
+ if (chords_[selectedChord_].type == CTYPE_INTERVAL)
+ {
+ onEncoderChangedEditParam(&enc, selParam, 1, CPARAM_INT_SPRDUPDOWN);
+ onEncoderChangedEditParam(&enc, selParam, 2, CPARAM_INT_QUARTVOICE);
+ }
+ }
+
+ omxDisp.setDirty();
+ omxLeds.setDirty();
+}
+
+// Put all params here to make it easy to switch order in pages
+void OmxModeChords::onEncoderChangedEditParam(Encoder::Update *enc, uint8_t selectedParmIndex, uint8_t targetParamIndex, uint8_t paramType)
+{
+ if (selectedParmIndex != targetParamIndex)
+ return;
+
+ auto amtSlow = enc->accel(1);
+ auto amtFast = enc->accel(5);
+
+ bool triggerChord = false;
+
+ switch (paramType)
+ {
+ case CPARAM_UIMODE:
+ {
+ uiMode_ = constrain(uiMode_ + amtSlow, 0, 1);
+ if (amtSlow != 0)
+ {
+ allNotesOff();
+ // omxUtil.allOff();
+ }
+ }
+ break;
+ case CPARAM_MAN_STRUM:
+ {
+ if (mode_ == CHRDMODE_MANSTRUM)
+ {
+ if (enc->dir() < 0)
+ {
+ mode_ = CHRDMODE_PLAY;
+ }
+ }
+ else
+ {
+ if (enc->dir() > 0)
+ {
+ mode_ = CHRDMODE_MANSTRUM;
+ }
+ }
+ }
+ break;
+ case CPARAM_CHORD_TYPE:
+ {
+ if (amtSlow != 0)
+ {
+ if (chordEditMode_)
+ {
+ onChordEditOff();
+ enterChordEditMode();
+ }
+ else
+ {
+ onChordOff(selectedChord_);
+ }
+ }
+
+ chords_[selectedChord_].type = constrain(chords_[selectedChord_].type + amtSlow, 0, 1);
+ }
+ break;
+ case CPARAM_CHORD_MFX:
+ {
+ int8_t newMidiFx = constrain(chords_[selectedChord_].midiFx + amtSlow, -1, NUM_MIDIFX_GROUPS - 1);
+ selectMidiFxChordKey(newMidiFx, false);
+ }
+ break;
+ case CPARAM_BAS_NOTE:
+ case CPARAM_BAS_OCT:
+ {
+ chordUtil.onEncoderChangedEditParam(enc, &chords_[selectedChord_], selectedParmIndex, targetParamIndex, paramType);
+ triggerChord = amtSlow != 0;
+ }
+ break;
+ case CPARAM_BAS_CHORD:
+ {
+ uint8_t prevChord = chords_[selectedChord_].chord;
+ chords_[selectedChord_].chord = constrain(chords_[selectedChord_].chord + amtSlow, 0, kNumChordPatterns - 1);
+ if (chords_[selectedChord_].chord != prevChord)
+ {
+ triggerChord = true;
+
+ // constructChord(selectedChord_);
+ // omxDisp.displayMessage(kChordMsg[chords_[selectedChord_].chord]);
+ }
+ }
+ break;
+ case CPARAM_BAS_BALANCE:
+ {
+ chords_[selectedChord_].balance = constrain(chords_[selectedChord_].balance + amtFast, 0, (kNumChordBalance - 1) * 10);
+ activeChordBalance_ = getChordBalanceDetails(chords_[selectedChord_].balance);
+
+ // omxDisp.chordBalanceMsg(activeChordBalance_.type, activeChordBalance_.velMult, 10);
+
+ if (amtSlow != 0) // To see notes change on keyboard leds
+ {
+ constructChord(selectedChord_);
+ }
+ }
+ break;
+ case CPARAM_CHORD_MCHAN:
+ case CPARAM_CHORD_VEL:
+ case CPARAM_INT_NUMNOTES:
+ case CPARAM_INT_DEGREE:
+ case CPARAM_INT_OCTAVE:
+ case CPARAM_INT_TRANSPOSE:
+ case CPARAM_INT_SPREAD:
+ case CPARAM_INT_ROTATE:
+ case CPARAM_INT_VOICING:
+ case CPARAM_INT_SPRDUPDOWN:
+ case CPARAM_INT_QUARTVOICE:
+ {
+ chordUtil.onEncoderChangedEditParam(enc, &chords_[selectedChord_], selectedParmIndex, targetParamIndex, paramType);
+ }
+ break;
+ }
+
+ // Play chord if value changes
+ if (triggerChord)
+ {
+ if (mode_ == CHRDMODE_EDIT || chordEditMode_)
+ {
+ if (!chordEditMode_ && heldChord_ == selectedChord_)
+ {
+ onChordOff(selectedChord_);
+ onChordOn(selectedChord_);
+ }
+ else if (chordEditMode_ && activeChordEditNoteKey_ >= 0)
+ {
+ onChordEditOff();
+ onChordEditOn(selectedChord_);
+ }
+ else
+ {
+ constructChord(selectedChord_);
+ }
+ }
+ else
+ {
+ constructChord(selectedChord_);
+ }
+ }
+}
+
+void OmxModeChords::onEncoderChangedManStrum(Encoder::Update enc)
+{
+ if (chordNotes_[selectedChord_].active == false)
+ return;
+
+ auto amt = enc.accel(1);
+
+ // Serial.println("EncDelta: " + String(chordNotes_[selectedChord_].encDelta));
+
+ chordNotes_[selectedChord_].encDelta = chordNotes_[selectedChord_].encDelta + amt;
+
+ if (abs(chordNotes_[selectedChord_].encDelta) >= manStrumSensit_)
+ {
+
+ uint8_t numNotes = 0;
+
+ for (uint8_t i = 0; i < 6; i++)
+ {
+ if (chordNotes_[selectedChord_].notes[i] >= 0)
+ {
+ numNotes++;
+ }
+ }
+
+ // Serial.println("Do Note");
+ uint8_t velocity = midiSettings.defaultVelocity;
+
+ int8_t strumPos = chordNotes_[selectedChord_].strumPos;
+
+ // Serial.println("strumPos: " + String(strumPos));
+
+ if (strumPos >= 0 && strumPos < numNotes)
+ {
+ int note = chordNotes_[selectedChord_].notes[strumPos] + (chordNotes_[selectedChord_].octIncrement * 12);
+
+ if (note >= 0 && note <= 127)
+ {
+ uint32_t noteOnMicros = micros();
+ float noteLength = kNoteLengths[manStrumNoteLength_];
+ uint32_t noteOffMicros = noteOnMicros + (noteLength * clockConfig.step_micros);
+
+ pendingNoteOns.insert(note, velocity, chordNotes_[selectedChord_].channel, noteOnMicros, false);
+ pendingNoteOffs.insert(note, chordNotes_[selectedChord_].channel, noteOffMicros, false);
+
+ omxDisp.displayMessage(musicScale_->getFullNoteName(note));
+ omxDisp.setDirty();
+ omxLeds.setDirty();
+ }
+ }
+
+ if (chordNotes_[selectedChord_].encDelta > 0)
+ {
+ strumPos++;
+ }
+ else
+ {
+ strumPos--;
+ }
+
+ if (wrapManStrum_)
+ {
+ if (strumPos >= numNotes)
+ {
+ chordNotes_[selectedChord_].octIncrement++;
+ if (chordNotes_[selectedChord_].octIncrement > incrementManStrum_)
+ {
+ chordNotes_[selectedChord_].octIncrement = 0;
+ }
+ strumPos = 0;
+ }
+ if (strumPos < 0)
+ {
+
+ chordNotes_[selectedChord_].octIncrement--;
+ if (chordNotes_[selectedChord_].octIncrement < -incrementManStrum_)
+ {
+ chordNotes_[selectedChord_].octIncrement = 0;
+ }
+ strumPos = numNotes - 1;
+ }
+
+ chordNotes_[selectedChord_].strumPos = strumPos;
+ }
+ else
+ {
+ chordNotes_[selectedChord_].strumPos = constrain(strumPos, -1, 6); // Allow to be one outside of notes
+ }
+
+ // chordNotes_[selectedChord_].octIncrement = constrain(chordNotes_[selectedChord_].octIncrement, -8, 8);
+
+ chordNotes_[selectedChord_].encDelta = 0;
+ }
+}
+
+void OmxModeChords::onEncoderButtonDown()
+{
+ if (isSubmodeEnabled())
+ {
+ activeSubmode->onEncoderButtonDown();
+ return;
+ }
+
+ bool macroConsumesDisplay = false;
+ if (macroActive_ && activeMacro_ != nullptr)
+ {
+ macroConsumesDisplay = activeMacro_->consumesDisplay();
+ }
+
+ if (macroConsumesDisplay)
+ {
+ activeMacro_->onEncoderButtonDown();
+ return;
+ }
+
+ encoderSelect_ = !encoderSelect_;
+ omxDisp.setDirty();
+}
+
+void OmxModeChords::onEncoderButtonDownLong()
+{
+}
+
+void OmxModeChords::inMidiControlChange(byte channel, byte control, byte value)
+{
+ auto activeMacro = getActiveMacro();
+
+ if (activeMacro != nullptr)
+ {
+ activeMacro->inMidiControlChange(channel, control, value);
+ }
+}
+
+bool OmxModeChords::shouldBlockEncEdit()
+{
+ if (isSubmodeEnabled())
+ {
+ return activeSubmode->shouldBlockEncEdit();
+ }
+
+ if (macroActive_)
+ {
+ return true;
+ }
+
+ return false;
+}
+
+void OmxModeChords::onKeyUpdate(OMXKeypadEvent e)
+{
+ uint8_t thisKey = e.key();
+
+ if (isSubmodeEnabled())
+ {
+ if (activeSubmode->onKeyUpdate(e))
+ return;
+ }
+
+ // Aux double click toggle macro
+ if (!isSubmodeEnabled() && midiMacroConfig.midiMacro > 0)
+ {
+ if (!macroActive_)
+ {
+ // Enter Macro Mode
+ if (!e.down() && thisKey == 0 && e.clicks() == 2)
+ {
+ auxDown_ = false;
+ midiSettings.midiAUX = false;
+
+ activeMacro_ = getActiveMacro();
+ if (activeMacro_ != nullptr)
+ {
+ macroActive_ = true;
+ activeMacro_->setEnabled(true);
+ activeMacro_->setScale(musicScale_);
+ omxLeds.setDirty();
+ omxDisp.setDirty();
+ return;
+ }
+ return;
+ }
+ }
+ else // Macro mode active
+ {
+ if (!e.down() && thisKey == 0 && e.clicks() == 2)
+ {
+ // exit macro mode
+ if (activeMacro_ != nullptr)
+ {
+ activeMacro_->setEnabled(false);
+ activeMacro_ = nullptr;
+ }
+
+ auxDown_ = false;
+ midiSettings.midiAUX = false;
+ macroActive_ = false;
+ omxLeds.setDirty();
+ omxDisp.setDirty();
+ }
+ else
+ {
+ if (activeMacro_ != nullptr)
+ {
+ activeMacro_->onKeyUpdate(e);
+ }
+ }
+ return;
+ }
+ }
+
+ if (chordEditMode_)
+ {
+ onKeyUpdateChordEdit(e);
+ return;
+ }
+
+ if (onKeyUpdateSelMidiFX(e))
+ return;
+
+ if (e.held())
+ return;
+
+ // auto keyState = midiSettings.keyState;
+
+ auto params = getParams();
+
+ // AUX KEY
+ if (thisKey == 0)
+ {
+ if (e.down())
+ {
+ if (!macroActive_)
+ {
+ auxDown_ = true;
+ midiSettings.midiAUX = true;
+ }
+ }
+ else
+ {
+ auxDown_ = false;
+ midiSettings.midiAUX = false;
+
+ // Forces all arps to work.
+ for (uint8_t i = 0; i < NUM_MIDIFX_GROUPS; i++)
+ {
+ subModeMidiFx[i].setSelected(true);
+ }
+ }
+ omxLeds.setDirty();
+ omxDisp.setDirty();
+ return;
+ }
+
+ if (auxDown_) // Aux mode
+ {
+ if (e.down())
+ {
+ if (thisKey == 11 || thisKey == 12) // Change Octave
+ {
+ int amt = thisKey == 11 ? -1 : 1;
+ midiSettings.octave = constrain(midiSettings.octave + amt, -5, 4);
+ }
+ else if (!mfxQuickEdit_ && (thisKey == 1 || thisKey == 2)) // Change Param selection
+ {
+ if (thisKey == 1)
+ {
+ params->decrementParam();
+ }
+ else if (thisKey == 2)
+ {
+ params->incrementParam();
+ }
+ }
+ else if (thisKey == 3)
+ {
+ presetManager.configure(PRESETMODE_LOAD, selectedSave_, NUM_CHORD_SAVES, true);
+ enableSubmode(&presetManager);
+ return;
+ }
+ else if (thisKey == 4)
+ {
+ presetManager.configure(PRESETMODE_SAVE, selectedSave_, NUM_CHORD_SAVES, true);
+ enableSubmode(&presetManager);
+ return;
+ }
+ }
+ }
+ else
+ {
+ bool keyConsumed = false;
+ if ((mode_ == CHRDMODE_PLAY || mode_ == CHRDMODE_EDIT) && uiMode_ == CUIMODE_SPLIT)
+ {
+ // Split UI Mode
+ if (thisKey >= 19 || (thisKey >= 6 && thisKey < 11)) // Check if key is on right side starting from C2(19)
+ {
+ keyConsumed = true;
+
+ uint8_t adjKeyIndex = thisKey >= 19 ? thisKey - 7 : thisKey - 5; // Pretends keys are down an octave
+
+ // If we're in edit mode and holding down a chord of the basic type, then we can edit the chord
+ // rather than play a note
+ if (mode_ == CHRDMODE_EDIT && heldChord_ >= 0 && chords_[heldChord_].type == CTYPE_BASIC)
+ {
+ if (e.down())
+ {
+ int adjnote = notes[adjKeyIndex] + (midiSettings.octave * 12);
+
+ // If valid note, we edit the chord setting the root note and octave, stop currently playing chord
+ // and turn on new chords
+ if (adjnote >= 0 && adjnote <= 127)
+ {
+ onChordOff(selectedChord_);
+ // delay(10);
+ onChordEditOff();
+ // delay(10);
+ chords_[selectedChord_].note = adjnote % 12;
+ chords_[selectedChord_].basicOct = (adjnote / 12) - 5;
+ activeChordEditNoteKey_ = thisKey;
+ onChordEditOn(selectedChord_);
+ }
+ }
+ else
+ {
+ if (thisKey == activeChordEditNoteKey_)
+ {
+ onChordEditOff();
+ activeChordEditNoteKey_ = -1;
+ }
+ }
+ }
+ // Not holding a chord in edit mode, play a note
+ else
+ {
+ activeChordEditNoteKey_ = -1;
+ lastKeyWasKeyboard_ = true;
+
+ if (e.down())
+ {
+ splitNoteOn(adjKeyIndex);
+ }
+ else
+ {
+ splitNoteOff(adjKeyIndex);
+ }
+ }
+ }
+ }
+
+ // [F1][F2] [PLAY][EDIT][PRESET] [STRUM]
+ // [C1][C2][C3][C4][C5] [C6] [C7]
+
+ if (!keyConsumed)
+ {
+ if (funcKeyMode_ == FUNCKEYMODE_NONE)
+ {
+ // Key Down, no function keys, no aux
+ if (e.down())
+ {
+ if (thisKey == 3)
+ {
+ mode_ = CHRDMODE_PLAY;
+ setSelPageAndParam(CHRDPAGE_NOTES, 0);
+ encoderSelect_ = true;
+ omxDisp.displayMessage("Play");
+ }
+ else if (thisKey == 4)
+ {
+ mode_ = CHRDMODE_EDIT;
+ setSelPageAndParam(CHRDPAGE_2, 0);
+ encoderSelect_ = true;
+ omxDisp.displayMessage("Edit");
+ allNotesOff();
+ }
+ // else if (thisKey == 5)
+ // {
+ // mode_ = CHRDMODE_PRESET;
+ // omxDisp.displayMessage("Preset");
+ // allNotesOff();
+ // }
+ else if (thisKey == 5)
+ {
+ mode_ = CHRDMODE_MANSTRUM;
+ omxDisp.displayMessage("Manual Strum");
+ allNotesOff();
+ }
+ if (thisKey >= 11)
+ {
+ if (mode_ == CHRDMODE_PLAY) // Play
+ {
+ selectedChord_ = thisKey - 11;
+ heldChord_ = thisKey - 11;
+ lastKeyWasKeyboard_ = false;
+ onChordOn(thisKey - 11);
+ }
+ else if (mode_ == CHRDMODE_EDIT) // Edit
+ {
+ selectedChord_ = thisKey - 11;
+ heldChord_ = thisKey - 11;
+ lastKeyWasKeyboard_ = false;
+ onChordOn(thisKey - 11);
+ }
+ // else if (mode_ == CHRDMODE_PRESET) // Preset
+ // {
+ // selectedChord_ = thisKey - 11;
+ // heldChord_ = thisKey - 11;
+ // lastKeyWasKeyboard_ = false;
+ // onChordOn(thisKey - 11);
+ // }
+ else if (mode_ == CHRDMODE_MANSTRUM) // Manual Strum
+ {
+ selectedChord_ = thisKey - 11;
+ heldChord_ = thisKey - 11;
+ lastKeyWasKeyboard_ = false;
+ onManualStrumOn(selectedChord_);
+ return;
+ }
+ }
+ }
+ else
+ {
+ if (thisKey >= 11)
+ {
+ if (thisKey - 11 == heldChord_)
+ {
+ heldChord_ = -1;
+ }
+
+ onChordOff(thisKey - 11);
+ }
+ }
+ }
+ else // Function key held
+ {
+ // Alt way to enter manual strum useful in split screen view
+ // if(mode_ == CHRDMODE_PLAY && funcKeyMode_ == FUNCKEYMODE_F1 && e.down() && thisKey == 3)
+ // if(funcKeyMode_ == FUNCKEYMODE_F1 && e.down() && thisKey == 3)
+ // {
+ // mode_ = CHRDMODE_MANSTRUM;
+ // omxDisp.displayMessage("Manual Strum");
+ // allNotesOff();
+ // return;
+ // }
+
+ if (e.down() && thisKey >= 11)
+ {
+ // --- PLAY MODE ---
+ if (mode_ == CHRDMODE_PLAY)
+ {
+ // if (funcKeyMode_ == FUNCKEYMODE_F1)
+ // {
+ // }
+ // else if (funcKeyMode_ == FUNCKEYMODE_F2)
+ // {
+ // if (pasteSelectedChordTo(thisKey - 11))
+ // {
+ // omxDisp.displayMessageTimed("Copied to " + String(thisKey - 11), 5);
+ // }
+ // }
+ }
+ // --- EDIT MODE ---
+ else if (mode_ == CHRDMODE_EDIT)
+ {
+ if (funcKeyMode_ == FUNCKEYMODE_F1)
+ {
+ selectedChord_ = thisKey - 11;
+ lastKeyWasKeyboard_ = false;
+ enterChordEditMode();
+ return;
+ }
+ else if (funcKeyMode_ == FUNCKEYMODE_F2)
+ {
+ if (pasteSelectedChordTo(thisKey - 11))
+ {
+ omxDisp.displayMessageTimed("Copied to " + String(thisKey - 11), 5);
+ }
+ }
+ }
+ // --- PRESET MODE ---
+ // else if (mode_ == CHRDMODE_PRESET)
+ // {
+ // if (funcKeyMode_ == FUNCKEYMODE_F1)
+ // {
+ // // Autosave your current preset unless you are reloading the current preset
+ // if(thisKey - 11 != selectedSave_)
+ // {
+ // savePreset(selectedSave_);
+ // }
+ // if (loadPreset(thisKey - 11))
+ // {
+ // omxDisp.displayMessageTimed("Load " + String(thisKey - 11), 5);
+ // }
+ // }
+ // else if (funcKeyMode_ == FUNCKEYMODE_F2)
+ // {
+ // if (savePreset(thisKey - 11))
+ // {
+ // omxDisp.displayMessageTimed("Saved to " + String(thisKey - 11), 5);
+ // }
+ // }
+ // }
+ // --- STRUM MODE ---
+ else if (mode_ == CHRDMODE_MANSTRUM) // Manual Strum
+ {
+ // if (funcKeyMode_ == FUNCKEYMODE_F1)
+ // {
+ // selectedChord_ = thisKey - 11;
+ // enterChordEditMode();
+ // return;
+ // }
+ // else if (funcKeyMode_ == FUNCKEYMODE_F2)
+ // {
+ // if (pasteSelectedChordTo(thisKey - 11))
+ // {
+ // omxDisp.displayMessageTimed("Copied to " + String(thisKey - 11), 5);
+ // }
+ // }
+ }
+ }
+ }
+ }
+ }
+
+ omxLeds.setDirty();
+ omxDisp.setDirty();
+}
+
+void OmxModeChords::onKeyUpdateChordEdit(OMXKeypadEvent e)
+{
+ if (e.held())
+ return;
+
+ uint8_t thisKey = e.key();
+
+ getParams(); // Sync params;
+
+ // auto params = getParams();
+
+ // AUX KEY
+ if (thisKey == 0)
+ {
+ if (e.down())
+ {
+ // Exit Chord Edit Mode
+ onChordEditOff();
+ if (mode_ == CHRDMODE_PLAY)
+ {
+ setSelPageAndParam(CHRDPAGE_NOTES, 0);
+ }
+
+ encoderSelect_ = true;
+ chordEditMode_ = false;
+ activeChordEditDegree_ = -1;
+ activeChordEditNoteKey_ = -1;
+ }
+
+ omxLeds.setDirty();
+ omxDisp.setDirty();
+ return;
+ }
+
+ if (chords_[selectedChord_].type == CTYPE_INTERVAL)
+ {
+ if (e.down())
+ {
+ if (chordEditParam_ == 0)
+ {
+ if (thisKey == 1) // Select Root
+ {
+ setSelPageAndParam(CHRDPAGE_GBL1, 1);
+ encoderSelect_ = false;
+ }
+ if (thisKey == 2) // Select Scale
+ {
+ setSelPageAndParam(CHRDPAGE_SCALES, 2);
+ encoderSelect_ = false;
+ }
+ if (thisKey == 3) // Octave
+ {
+ chordEditParam_ = 1;
+ setSelPageAndParam(CHRDPAGE_2, 2);
+ encoderSelect_ = false;
+ }
+ else if (thisKey == 4) // Transpose
+ {
+ chordEditParam_ = 2;
+ setSelPageAndParam(CHRDPAGE_2, 3);
+ encoderSelect_ = false;
+ }
+ else if (thisKey == 5) // Spread
+ {
+ chordEditParam_ = 3;
+ setSelPageAndParam(CHRDPAGE_3, 0);
+ encoderSelect_ = false;
+ }
+ else if (thisKey == 6) // Rotate
+ {
+ chordEditParam_ = 4;
+ setSelPageAndParam(CHRDPAGE_3, 1);
+ encoderSelect_ = false;
+ }
+ else if (thisKey == 7) // Voicing
+ {
+ chordEditParam_ = 5;
+ setSelPageAndParam(CHRDPAGE_3, 2);
+ encoderSelect_ = false;
+ }
+ else if (thisKey == 10) // Show Chord Notes
+ {
+ setSelPageAndParam(CHRDPAGE_NOTES, 0);
+ encoderSelect_ = true;
+ }
+ else if (thisKey >= 11 && thisKey < 15) // Num of Notes
+ {
+ chords_[selectedChord_].numNotes = (thisKey - 11) + 1;
+ setSelPageAndParam(CHRDPAGE_2, 0);
+ encoderSelect_ = false;
+ }
+ else if (thisKey == 15) // Spread Up Down
+ {
+ chords_[selectedChord_].spreadUpDown = !chords_[selectedChord_].spreadUpDown;
+ setSelPageAndParam(CHRDPAGE_4, 0);
+ encoderSelect_ = false;
+ omxDisp.displayMessage(chords_[selectedChord_].spreadUpDown ? "SpdUpDn On" : "SpdUpDn Off");
+ }
+ else if (thisKey == 16) // Quartal Voicing
+ {
+ chords_[selectedChord_].quartalVoicing = !chords_[selectedChord_].quartalVoicing;
+ setSelPageAndParam(CHRDPAGE_4, 1);
+ encoderSelect_ = false;
+ omxDisp.displayMessage(chords_[selectedChord_].quartalVoicing ? "Quartal On" : "Quartal Off");
+ }
+ else if (thisKey >= 19)
+ {
+ chords_[selectedChord_].degree = thisKey - 19;
+ // params_.setSelPageAndParam(CHRDPAGE_2, 1);
+ // encoderSelect_ = false;
+ onChordEditOff();
+ onChordEditOn(selectedChord_);
+ activeChordEditDegree_ = thisKey - 19;
+ }
+ }
+ else if (chordEditParam_ == 1) // Octave
+ {
+ // chords_[selectedChord_].octave = constrain(chords_[selectedChord_].octave + amt, -2, 2);
+ if (thisKey >= 11 && thisKey <= 15)
+ {
+ chords_[selectedChord_].octave = thisKey - 11 - 2;
+ }
+ }
+ else if (chordEditParam_ == 2) // Transpose
+ {
+ // chords_[selectedChord_].transpose = constrain(chords_[selectedChord_].transpose + amt, -7, 7);
+ if (thisKey >= 11 && thisKey <= 25)
+ {
+ chords_[selectedChord_].transpose = thisKey - 11 - 7;
+ }
+ }
+ else if (chordEditParam_ == 3) // Spread
+ {
+ // chords_[selectedChord_].spread = constrain(chords_[selectedChord_].spread + amt, -2, 2);
+ if (thisKey >= 11 && thisKey <= 15)
+ {
+ chords_[selectedChord_].spread = thisKey - 11 - 2;
+ }
+ }
+ else if (chordEditParam_ == 4) // Rotate
+ {
+ // chords_[selectedChord_].rotate = constrain(chords_[selectedChord_].rotate + amt, 0, 4);
+ if (thisKey >= 11 && thisKey <= 15)
+ {
+ chords_[selectedChord_].rotate = thisKey - 11;
+ }
+ }
+ else if (chordEditParam_ == 5) // Voicing
+ {
+ // chords_[selectedChord_].voicing = constrain(chords_[selectedChord_].octave + amt, 0, 7);
+ if (thisKey >= 11 && thisKey <= 18)
+ {
+ chords_[selectedChord_].voicing = thisKey - 11;
+ }
+ }
+ }
+ else
+ {
+ if (thisKey >= 3 && thisKey <= 7)
+ {
+ chordEditParam_ = 0;
+ }
+ else if (thisKey >= 19)
+ {
+ if (thisKey - 19 == activeChordEditDegree_)
+ {
+ onChordEditOff();
+ activeChordEditDegree_ = -1;
+ }
+ }
+ }
+ }
+ else if (chords_[selectedChord_].type == CTYPE_BASIC)
+ {
+ if (e.down())
+ {
+ if (thisKey == 11 || thisKey == 26)
+ {
+ // Change octave
+ int amt = thisKey == 11 ? -1 : 1;
+ midiSettings.octave = constrain(midiSettings.octave + amt, -5, 4);
+ }
+ else
+ {
+ int adjnote = notes[thisKey] + (midiSettings.octave * 12);
+
+ if (adjnote >= 0 && adjnote <= 127)
+ {
+ chords_[selectedChord_].note = adjnote % 12;
+ chords_[selectedChord_].basicOct = (adjnote / 12) - 5;
+ activeChordEditNoteKey_ = thisKey;
+ onChordEditOff();
+ onChordEditOn(selectedChord_);
+ }
+ }
+ }
+ else
+ {
+ if (thisKey == activeChordEditNoteKey_)
+ {
+ onChordEditOff();
+ activeChordEditNoteKey_ = -1;
+ }
+ }
+ }
+
+ omxLeds.setDirty();
+ omxDisp.setDirty();
+}
+
+void OmxModeChords::enterChordEditMode()
+{
+ omxDisp.displayMessageTimed("Editing " + String(selectedChord_), 5);
+ constructChord(selectedChord_);
+
+ allNotesOff();
+
+ chordEditMode_ = true;
+ chordEditParam_ = 0;
+ heldChord_ = -1;
+ activeChordEditDegree_ = -1;
+ activeChordEditNoteKey_ = -1;
+ setSelPageAndParam(CHRDPAGE_2, 0);
+ encoderSelect_ = true;
+ omxLeds.setDirty();
+ omxDisp.setDirty();
+}
+
+void OmxModeChords::onKeyHeldUpdate(OMXKeypadEvent e)
+{
+ if (isSubmodeEnabled())
+ {
+ if (activeSubmode->onKeyHeldUpdate(e))
+ return;
+ }
+
+ if (onKeyHeldSelMidiFX(e))
+ return;
+}
+
+void OmxModeChords::enableSubmode(SubmodeInterface *subMode)
+{
+ if (activeSubmode != nullptr)
+ {
+ activeSubmode->setEnabled(false);
+ }
+
+ auxDown_ = false;
+ midiSettings.midiAUX = false;
+
+ activeSubmode = subMode;
+ activeSubmode->setEnabled(true);
+ omxDisp.setDirty();
+}
+void OmxModeChords::disableSubmode()
+{
+ if (activeSubmode != nullptr)
+ {
+ activeSubmode->setEnabled(false);
+ }
+
+ midiSettings.midiAUX = false;
+ mfxQuickEdit_ = false;
+ activeSubmode = nullptr;
+ omxDisp.setDirty();
+}
+
+bool OmxModeChords::isSubmodeEnabled()
+{
+ if (activeSubmode == nullptr)
+ return false;
+
+ if (activeSubmode->isEnabled() == false)
+ {
+ disableSubmode();
+ auxDown_ = false;
+ midiSettings.midiAUX = false;
+ return false;
+ }
+
+ return true;
+}
+
+bool OmxModeChords::getEncoderSelect()
+{
+ if (chordEditMode_)
+ {
+ return encoderSelect_ && !auxDown_ && activeChordEditDegree_ < 0 && activeChordEditNoteKey_ < 0;
+ }
+
+ return encoderSelect_ && !auxDown_ && heldChord_ < 0;
+}
+
+ParamManager *OmxModeChords::getParams()
+{
+ if (chords_[selectedChord_].type == CTYPE_BASIC)
+ {
+ basicParams_.setPageEnabled(CHRDPAGE_3, chords_[selectedChord_].chord == kCustomChordPattern);
+ intervalParams_.setSelPageAndParam(basicParams_.getSelPage(), basicParams_.getSelParam());
+
+ return &basicParams_;
+ }
+ else
+ {
+ basicParams_.setSelPageAndParam(intervalParams_.getSelPage(), intervalParams_.getSelParam());
+ return &intervalParams_;
+ }
+}
+
+void OmxModeChords::setSelPageAndParam(int8_t newPage, int8_t newParam)
+{
+ auto params = getParams();
+ params->setSelPageAndParam(newPage, newParam);
+ getParams(); // to sync the params
+}
+
+bool OmxModeChords::onKeyUpdateSelMidiFX(OMXKeypadEvent e)
+{
+ int thisKey = e.key();
+
+ bool keyConsumed = false;
+
+ uint8_t mfxdex = lastKeyWasKeyboard_ ? mfxIndex_ : chords_[selectedChord_].midiFx;
+
+ if (!e.held())
+ {
+ if (!e.down() && e.clicks() == 2 && thisKey >= 6 && thisKey < 11)
+ {
+ if (auxDown_) // Aux mode
+ {
+ enableSubmode(&subModeMidiFx[thisKey - 6]);
+ keyConsumed = true;
+ }
+ }
+
+ if (e.down() && thisKey != 0)
+ {
+ if (auxDown_) // Aux mode
+ {
+ if (mfxQuickEdit_ && thisKey == 1)
+ {
+ subModeMidiFx[quickEditMfxIndex_].selectPrevMFXSlot();
+ }
+ else if (mfxQuickEdit_ && thisKey == 2)
+ {
+ subModeMidiFx[quickEditMfxIndex_].selectNextMFXSlot();
+ }
+ else if (thisKey == 5)
+ {
+ keyConsumed = true;
+ // Turn off midiFx
+ if (lastKeyWasKeyboard_)
+ {
+ selectMidiFx(127, true);
+ }
+ else
+ {
+ selectMidiFxChordKey(127, true);
+ }
+ }
+ else if (thisKey >= 6 && thisKey < 11)
+ {
+ keyConsumed = true;
+ if (lastKeyWasKeyboard_)
+ {
+ selectMidiFx(thisKey - 6, true);
+ }
+ else
+ {
+ selectMidiFxChordKey(thisKey - 6, true);
+ }
+ }
+ else if (thisKey == 20) // MidiFX Passthrough
+ {
+ keyConsumed = true;
+ if (mfxdex < NUM_MIDIFX_GROUPS)
+ {
+ enableSubmode(&subModeMidiFx[mfxdex]);
+ subModeMidiFx[mfxdex].enablePassthrough();
+ mfxQuickEdit_ = true;
+ quickEditMfxIndex_ = mfxdex;
+ midiSettings.midiAUX = false;
+ }
+ else
+ {
+ omxDisp.displayMessage(mfxOffMsg);
+ }
+ }
+ else if (thisKey == 22) // Goto arp params
+ {
+ keyConsumed = true;
+ if (mfxdex < NUM_MIDIFX_GROUPS)
+ {
+ enableSubmode(&subModeMidiFx[mfxdex]);
+ subModeMidiFx[mfxdex].gotoArpParams();
+ auxDown_ = false;
+ midiSettings.midiAUX = false;
+ }
+ else
+ {
+ omxDisp.displayMessage(mfxOffMsg);
+ }
+ }
+ else if (thisKey == 23) // Next arp pattern
+ {
+ keyConsumed = true;
+ if (mfxdex < NUM_MIDIFX_GROUPS)
+ {
+ subModeMidiFx[mfxdex].nextArpPattern();
+ }
+ else
+ {
+ omxDisp.displayMessage(mfxOffMsg);
+ }
+ }
+ else if (thisKey == 24) // Next arp octave
+ {
+ keyConsumed = true;
+ if (mfxdex < NUM_MIDIFX_GROUPS)
+ {
+ subModeMidiFx[mfxdex].nextArpOctRange();
+ }
+ else
+ {
+ omxDisp.displayMessage(mfxOffMsg);
+ }
+ }
+ else if (thisKey == 25)
+ {
+ keyConsumed = true;
+ if (mfxdex < NUM_MIDIFX_GROUPS)
+ {
+ subModeMidiFx[mfxdex].toggleArpHold();
+
+ if (subModeMidiFx[mfxdex].isArpHoldOn())
+ {
+ omxDisp.displayMessageTimed("Arp Hold: On", 5);
+ }
+ else
+ {
+ omxDisp.displayMessageTimed("Arp Hold: Off", 5);
+ }
+ }
+ else
+ {
+ omxDisp.displayMessage(mfxOffMsg);
+ }
+ }
+ else if (thisKey == 26)
+ {
+ keyConsumed = true;
+ if (mfxdex < NUM_MIDIFX_GROUPS)
+ {
+ subModeMidiFx[mfxdex].toggleArp();
+
+ if (subModeMidiFx[mfxdex].isArpOn())
+ {
+ omxDisp.displayMessageTimed("Arp On", 5);
+ }
+ else
+ {
+ omxDisp.displayMessageTimed("Arp Off", 5);
+ }
+ }
+ else
+ {
+ omxDisp.displayMessage(mfxOffMsg);
+ }
+ }
+ }
+ }
+ }
+
+ return keyConsumed;
+}
+
+bool OmxModeChords::onKeyHeldSelMidiFX(OMXKeypadEvent e)
+{
+ int thisKey = e.key();
+
+ bool keyConsumed = false;
+
+ if (auxDown_) // Aux mode
+ {
+ // Enter MidiFX mode
+ if (thisKey >= 6 && thisKey < 11)
+ {
+ keyConsumed = true;
+ enableSubmode(&subModeMidiFx[thisKey - 6]);
+ }
+ }
+
+ return keyConsumed;
+}
+
+void OmxModeChords::doNoteOn(int noteNumber, uint8_t midifx, uint8_t velocity, uint8_t midiChannel)
+{
+ if (noteNumber < 0 || noteNumber > 127)
+ return;
+
+ bool trackerFound = false;
+
+ for (uint8_t i = 0; i < noteOffTracker.size(); i++)
+ {
+ if (noteOffTracker[i].noteNumber == noteNumber && noteOffTracker[i].midiChannel == midiChannel - 1)
+ {
+ // Serial.println("Tracker found " + String(noteNumber));
+ noteOffTracker[i].triggerCount = noteOffTracker[i].triggerCount + 1;
+ // Serial.println("triggerCount: " + String(noteOffTracker[i].triggerCount));
+ trackerFound = true;
+ break;
+ }
+ }
+
+ if (!trackerFound && noteOffTracker.size() == kMaxNoteTrackerSize)
+ return; // Too many notes
+
+ if (!trackerFound)
+ {
+ // Serial.println("Tracker not found ");
+ NoteTracker tracker;
+ tracker.noteNumber = noteNumber;
+ tracker.midiChannel = midiChannel - 1;
+ tracker.triggerCount = 1;
+ noteOffTracker.push_back(tracker);
+ trackerFound = true;
+ }
+
+ // MidiNoteGroup noteGroup = omxUtil.midiNoteOn2(musicScale, keyIndex, midiSettings.defaultVelocity, sysSettings.midiChannel);
+ // if(noteGroup.noteNumber == 255) return;
+
+ MidiNoteGroup noteGroup;
+
+ noteGroup.noteOff = false;
+ noteGroup.noteNumber = noteNumber;
+ noteGroup.prevNoteNumber = noteNumber;
+ noteGroup.velocity = velocity;
+ noteGroup.channel = midiChannel;
+ noteGroup.unknownLength = true;
+ noteGroup.stepLength = 0;
+ noteGroup.sendMidi = true;
+ noteGroup.sendCV = false;
+ noteGroup.noteonMicros = micros();
+
+ // Serial.println("doNoteOn: " + String(noteGroup.noteNumber));
+
+ if (midifx < NUM_MIDIFX_GROUPS)
+ {
+ subModeMidiFx[midifx].noteInput(noteGroup);
+ // subModeMidiFx.noteInput(noteGroup);
+ }
+ else
+ {
+ onNotePostFX(noteGroup);
+ }
+}
+
+void OmxModeChords::doNoteOff(int noteNumber, uint8_t midifx, uint8_t midiChannel)
+{
+ if (noteNumber < 0 || noteNumber > 127)
+ return;
+
+ bool trackerFound = false;
+ bool doNoteOff = false;
+
+ for (uint8_t i = 0; i < noteOffTracker.size(); i++)
+ {
+ if (noteOffTracker[i].noteNumber == noteNumber && noteOffTracker[i].midiChannel == midiChannel - 1)
+ {
+ // Serial.println("Tracker found " + String(noteNumber));
+ // Serial.println("triggerCount " + String(noteOffTracker[i].triggerCount));
+
+ noteOffTracker[i].triggerCount = noteOffTracker[i].triggerCount - 1;
+ if (noteOffTracker[i].triggerCount <= 0)
+ {
+ // Serial.println("Do Note Off");
+ doNoteOff = true;
+ }
+ trackerFound = true;
+
+ // Serial.println("triggerCount " + String(noteOffTracker[i].triggerCount));
+ break;
+ }
+ }
+
+ if (doNoteOff)
+ {
+ auto it = noteOffTracker.begin();
+ while (it != noteOffTracker.end())
+ {
+ // remove matching note numbers
+ if (it->triggerCount <= 0)
+ {
+ // Serial.println("Erasing");
+ it = noteOffTracker.erase(it);
+ }
+ else
+ {
+ ++it;
+ }
+ }
+ }
+
+ if (!trackerFound || !doNoteOff)
+ return; // No note off tracker found.
+
+ // Serial.println("Doing Note Off");
+
+ // MidiNoteGroup noteGroup = omxUtil.midiNoteOff2(keyIndex, sysSettings.midiChannel);
+
+ // if(noteGroup.noteNumber == 255) return;
+
+ MidiNoteGroup noteGroup;
+
+ noteGroup.noteOff = true;
+ noteGroup.noteNumber = noteNumber;
+ noteGroup.prevNoteNumber = noteNumber;
+ noteGroup.velocity = 0;
+ noteGroup.channel = midiChannel;
+ noteGroup.unknownLength = true;
+ noteGroup.stepLength = 0;
+ noteGroup.sendMidi = true;
+ noteGroup.sendCV = false;
+ noteGroup.noteonMicros = micros();
+
+ // Serial.println("doNoteOff: " + String(noteGroup.noteNumber));
+
+ noteGroup.unknownLength = true;
+ noteGroup.prevNoteNumber = noteGroup.noteNumber;
+
+ if (midifx >= 0 && midifx < NUM_MIDIFX_GROUPS)
+ {
+ subModeMidiFx[midifx].noteInput(noteGroup);
+ // subModeMidiFx.noteInput(noteGroup);
+ }
+ else
+ {
+ onNotePostFX(noteGroup);
+ }
+}
+
+void OmxModeChords::splitNoteOn(uint8_t keyIndex)
+{
+ MidiNoteGroup noteGroup = omxUtil.midiNoteOn2(musicScale_, keyIndex, midiSettings.defaultVelocity, sysSettings.midiChannel);
+ doNoteOn(noteGroup.noteNumber, mfxIndex_, noteGroup.velocity, noteGroup.channel);
+
+ // if(noteGroup.noteNumber > 127) return;
+
+ // bool trackerFound = false;
+
+ // for(uint8_t i = 0; i < noteOffTracker.size(); i++)
+ // {
+ // if(noteOffTracker[i].noteNumber == noteGroup.noteNumber && noteOffTracker[i].midiChannel == noteGroup.channel - 1)
+ // {
+ // noteOffTracker[i].triggerCount++;
+ // trackerFound = true;
+ // break;
+ // }
+ // }
+
+ // if(!trackerFound && noteOffTracker.size() == kMaxNoteTrackerSize) return; // Too many notes
+
+ // if(!trackerFound)
+ // {
+ // NoteTracker tracker;
+ // tracker.noteNumber = noteGroup.noteNumber;
+ // tracker.midiChannel = noteGroup.channel - 1;
+ // tracker.triggerCount = 1;
+ // noteOffTracker.push_back(tracker);
+ // trackerFound = true;
+ // }
+
+ // noteGroup.unknownLength = true;
+ // noteGroup.prevNoteNumber = noteGroup.noteNumber;
+
+ // if (mfxIndex_ < NUM_MIDIFX_GROUPS)
+ // {
+ // subModeMidiFx[mfxIndex_].noteInput(noteGroup);
+ // }
+ // else
+ // {
+ // onNotePostFX(noteGroup);
+ // }
+}
+void OmxModeChords::splitNoteOff(uint8_t keyIndex)
+{
+ MidiNoteGroup noteGroup = omxUtil.midiNoteOff2(keyIndex, sysSettings.midiChannel);
+ doNoteOff(noteGroup.noteNumber, mfxIndex_, noteGroup.channel);
+
+ // if(noteGroup.noteNumber > 127) return;
+
+ // noteGroup.unknownLength = true;
+ // noteGroup.prevNoteNumber = noteGroup.noteNumber;
+
+ // if (mfxIndex_ < NUM_MIDIFX_GROUPS)
+ // {
+ // subModeMidiFx[mfxIndex_].noteInput(noteGroup);
+ // }
+ // else
+ // {
+ // onNotePostFX(noteGroup);
+ // }
+}
+
+void OmxModeChords::onNotePostFX(MidiNoteGroup note)
+{
+ if (note.noteOff)
+ {
+ // Serial.println("OmxModeMidiKeyboard::onNotePostFX noteOff: " + String(note.noteNumber));
+
+ if (note.sendMidi)
+ {
+ MM::sendNoteOff(note.noteNumber, note.velocity, note.channel);
+ }
+ if (note.sendCV)
+ {
+ cvNoteUtil.cvNoteOff(note.noteNumber);
+ }
+ }
+ else
+ {
+ if (note.unknownLength == false)
+ {
+ uint32_t noteOnMicros = note.noteonMicros; // TODO Might need to be set to current micros
+ pendingNoteOns.insert(note.noteNumber, note.velocity, note.channel, noteOnMicros, note.sendCV);
+
+ // Serial.println("StepLength: " + String(note.stepLength));
+
+ uint32_t noteOffMicros = noteOnMicros + (note.stepLength * clockConfig.step_micros);
+ pendingNoteOffs.insert(note.noteNumber, note.channel, noteOffMicros, note.sendCV);
+
+ // Serial.println("noteOnMicros: " + String(noteOnMicros));
+ // Serial.println("noteOffMicros: " + String(noteOffMicros));
+ }
+ else
+ {
+ // Serial.println("OmxModeMidiKeyboard::onNotePostFX noteOn: " + String(note.noteNumber));
+
+ if (note.sendMidi)
+ {
+ MM::sendNoteOn(note.noteNumber, note.velocity, note.channel);
+ }
+ if (note.sendCV)
+ {
+ cvNoteUtil.cvNoteOn(note.noteNumber);
+ }
+ }
+ }
+}
+
+void OmxModeChords::onPendingNoteOff(int note, int channel)
+{
+ // Serial.println("OmxModeEuclidean::onPendingNoteOff " + String(note) + " " + String(channel));
+ // subModeMidiFx.onPendingNoteOff(note, channel);
+
+ for (uint8_t i = 0; i < NUM_MIDIFX_GROUPS; i++)
+ {
+ subModeMidiFx[i].onPendingNoteOff(note, channel);
+ }
+}
+
+void OmxModeChords::updateLEDs()
+{
+ if (isSubmodeEnabled())
+ {
+ if (activeSubmode->updateLEDs())
+ return;
+ }
+
+ if (chordEditMode_)
+ {
+ updateLEDsChordEdit();
+ return;
+ }
+
+ bool blinkState = omxLeds.getBlinkState();
+
+ omxLeds.setAllLEDS(0, 0, 0);
+
+ if (auxDown_)
+ {
+ // Blink left/right keys for octave select indicators.
+ strip.setPixelColor(0, RED);
+ strip.setPixelColor(1, LIME);
+ strip.setPixelColor(2, MAGENTA);
+
+ strip.setPixelColor(3, BLUE); // Load
+ strip.setPixelColor(4, ORANGE); // Save
+
+ if (midiSettings.octave == 0)
+ {
+ strip.setPixelColor(11, colorConfig.octDnColor);
+ strip.setPixelColor(12, colorConfig.octUpColor);
+ }
+ else if (midiSettings.octave > 0)
+ {
+ bool blinkOctave = omxLeds.getBlinkPattern(midiSettings.octave);
+
+ strip.setPixelColor(11, colorConfig.octDnColor);
+ strip.setPixelColor(12, blinkOctave ? colorConfig.octUpColor : LEDOFF);
+ }
+ else
+ {
+ bool blinkOctave = omxLeds.getBlinkPattern(-midiSettings.octave);
+
+ strip.setPixelColor(11, blinkOctave ? colorConfig.octDnColor : LEDOFF);
+ strip.setPixelColor(12, colorConfig.octUpColor);
+ }
+
+ // MidiFX off
+ uint8_t mfxdex = lastKeyWasKeyboard_ ? mfxIndex_ : chords_[selectedChord_].midiFx;
+
+ strip.setPixelColor(5, (mfxdex >= NUM_MIDIFX_GROUPS ? colorConfig.selMidiFXGRPOffColor : colorConfig.midiFXGRPOffColor));
+
+ for (uint8_t i = 0; i < NUM_MIDIFX_GROUPS; i++)
+ {
+ auto mfxColor = (i == mfxdex) ? colorConfig.selMidiFXGRPColor : colorConfig.midiFXGRPColor;
+
+ strip.setPixelColor(6 + i, mfxColor);
+ }
+
+ strip.setPixelColor(20, mfxQuickEdit_ && blinkState ? LEDOFF : colorConfig.mfxQuickEdit);
+ strip.setPixelColor(22, colorConfig.gotoArpParams);
+ strip.setPixelColor(23, colorConfig.nextArpPattern);
+
+ if (mfxdex < NUM_MIDIFX_GROUPS)
+ {
+ uint8_t octaveRange = subModeMidiFx[mfxdex].getArpOctaveRange();
+ if (octaveRange == 0)
+ {
+ strip.setPixelColor(24, colorConfig.nextArpOctave);
+ }
+ else
+ {
+ // Serial.println("Blink Octave: " + String(octaveRange));
+ bool blinkOctave = omxLeds.getBlinkPattern(octaveRange);
+
+ strip.setPixelColor(24, blinkOctave ? colorConfig.nextArpOctave : LEDOFF);
+ }
+
+ bool isOn = subModeMidiFx[mfxdex].isArpOn() && blinkState;
+ bool isHoldOn = subModeMidiFx[mfxdex].isArpHoldOn();
+
+ strip.setPixelColor(25, isHoldOn ? colorConfig.arpHoldOn : colorConfig.arpHoldOff);
+ strip.setPixelColor(26, isOn ? colorConfig.arpOn : colorConfig.arpOff);
+ }
+ else
+ {
+ strip.setPixelColor(25, colorConfig.arpHoldOff);
+ strip.setPixelColor(26, colorConfig.arpOff);
+ }
+
+ return;
+ }
+
+ // Function Keys
+ if (funcKeyMode_ == FUNCKEYMODE_F3)
+ {
+ auto f3Color = blinkState ? LEDOFF : FUNKTHREE;
+ strip.setPixelColor(1, f3Color);
+ strip.setPixelColor(2, f3Color);
+ }
+ else
+ {
+ auto f1Color = (funcKeyMode_ == FUNCKEYMODE_F1 && blinkState) ? LEDOFF : FUNKONE;
+ strip.setPixelColor(1, f1Color);
+
+ auto f2Color = (funcKeyMode_ == FUNCKEYMODE_F2 && blinkState) ? LEDOFF : FUNKTWO;
+ strip.setPixelColor(2, f2Color);
+ }
+
+ strip.setPixelColor(3, mode_ == CHRDMODE_PLAY ? WHITE : kPlayColor);
+ strip.setPixelColor(4, mode_ == CHRDMODE_EDIT ? WHITE : kEditColor);
+ // strip.setPixelColor(5, mode_ == CHRDMODE_PRESET ? WHITE : kPresetColor);
+ strip.setPixelColor(5, mode_ == CHRDMODE_MANSTRUM ? WHITE : MAGENTA);
+
+ if (mode_ == CHRDMODE_PLAY || mode_ == CHRDMODE_MANSTRUM) // Play
+ {
+ for (uint8_t i = 0; i < 16; i++)
+ {
+ if (i == selectedChord_)
+ {
+ strip.setPixelColor(11 + i, (chordNotes_[i].active ? WHITE : CYAN));
+ }
+ else
+ {
+ strip.setPixelColor(11 + i, (chordNotes_[i].active ? WHITE : chords_[i].color));
+ }
+ }
+ }
+ else if (mode_ == CHRDMODE_EDIT) // Edit
+ {
+ for (uint8_t i = 0; i < 16; i++)
+ {
+ if (i == selectedChord_)
+ {
+ strip.setPixelColor(11 + i, (chordNotes_[i].active ? WHITE : CYAN));
+ }
+ else
+ {
+ strip.setPixelColor(11 + i, (chordNotes_[i].active ? WHITE : kEditColor));
+ }
+ }
+ }
+ // else if (mode_ == CHRDMODE_PRESET) // Preset
+ // {
+ // for (uint8_t i = 0; i < NUM_CHORD_SAVES; i++)
+ // {
+ // strip.setPixelColor(11 + i, (i == selectedSave_ ? WHITE : kPresetColor));
+ // }
+ // }
+
+ if ((mode_ == CHRDMODE_PLAY || mode_ == CHRDMODE_EDIT) && uiMode_ == CUIMODE_SPLIT)
+ {
+ bool blinkNote = activeChordEditNoteKey_ >= 0 ? omxLeds.getBlinkState() : true;
+
+ // Render scale colors and chord notes
+ for (int i = 1; i < LED_COUNT; i++)
+ {
+ if (i >= 19 || (i >= 6 && i < 11))
+ {
+ strip.setPixelColor(i, LEDOFF);
+
+ uint8_t adjKeyIndex = i >= 19 ? i - 7 : i - 5; // Pretends keys are down an octave
+
+ if (mode_ == CHRDMODE_EDIT && heldChord_ >= 0 && chords_[heldChord_].type == CTYPE_BASIC)
+ {
+ // Scale colors
+ auto keyColor = omxLeds.getKeyColor(musicScale_, adjKeyIndex);
+ if (keyColor != LEDOFF)
+ {
+ strip.setPixelColor(i, kChordEditNoteInScaleColor);
+ }
+
+ // Chord note colors
+ for (uint8_t ni = 0; ni < 6; ni++)
+ {
+ int note = chordNotes_[selectedChord_].notes[ni];
+
+ if (note >= 0 && note <= 127)
+ {
+ auto adjNote = notes[adjKeyIndex] + (midiSettings.octave * 12);
+
+ if (adjNote == note && blinkNote)
+ {
+ uint8_t vel = map(chordNotes_[selectedChord_].velocities[ni], 0, 127, 0, 255);
+
+ auto noteColor = ni == 0 ? strip.ColorHSV(kChordEditNoteChordHue, 50, vel) : strip.ColorHSV(kChordEditNoteChordHue, 255, vel);
+
+ strip.setPixelColor(i, noteColor);
+ }
+ }
+ }
+ }
+ else
+ {
+ if (midiSettings.midiKeyState[adjKeyIndex] >= 0)
+ {
+ strip.setPixelColor(i, LTCYAN);
+ }
+ else
+ {
+ // Scale colors
+ strip.setPixelColor(i, omxLeds.getKeyColor(musicScale_, adjKeyIndex));
+ }
+ }
+ }
+ }
+ }
+
+ if (isSubmodeEnabled())
+ {
+ bool blinkStateSlow = omxLeds.getSlowBlinkState();
+
+ auto auxColor = (blinkStateSlow ? RED : LEDOFF);
+ strip.setPixelColor(0, auxColor);
+ }
+}
+
+void OmxModeChords::updateLEDsChordEdit()
+{
+ bool blinkState = omxLeds.getBlinkState();
+
+ omxLeds.setAllLEDS(0, 0, 0);
+
+ strip.setPixelColor(0, RED); // EXIT
+
+ if (chords_[selectedChord_].type == CTYPE_BASIC)
+ {
+ bool blinkNote = activeChordEditNoteKey_ >= 0 ? omxLeds.getBlinkState() : true;
+
+ // Render scale colors and chord notes
+ for (int i = 1; i < LED_COUNT; i++)
+ {
+ // Scale colors
+ auto keyColor = omxLeds.getKeyColor(musicScale_, i);
+ if (keyColor != LEDOFF)
+ {
+ strip.setPixelColor(i, kChordEditNoteInScaleColor);
+ }
+
+ // Chord note colors
+ for (uint8_t ni = 0; ni < 6; ni++)
+ {
+ int note = chordNotes_[selectedChord_].notes[ni];
+
+ if (note >= 0 && note <= 127)
+ {
+ auto adjNote = notes[i] + (midiSettings.octave * 12);
+
+ if (adjNote == note && blinkNote)
+ {
+ uint8_t vel = map(chordNotes_[selectedChord_].velocities[ni], 0, 127, 0, 255);
+
+ auto noteColor = ni == 0 ? strip.ColorHSV(kChordEditNoteChordHue, 50, vel) : strip.ColorHSV(kChordEditNoteChordHue, 255, vel);
+
+ strip.setPixelColor(i, noteColor);
+ }
+ }
+ }
+ }
+
+ if (midiSettings.octave == 0)
+ {
+ strip.setPixelColor(11, colorConfig.octDnColor);
+ strip.setPixelColor(26, colorConfig.octUpColor);
+ }
+ else if (midiSettings.octave > 0)
+ {
+ bool blinkOctave = omxLeds.getBlinkPattern(midiSettings.octave);
+
+ strip.setPixelColor(11, colorConfig.octDnColor);
+ strip.setPixelColor(26, blinkOctave ? colorConfig.octUpColor : LEDOFF);
+ }
+ else
+ {
+ bool blinkOctave = omxLeds.getBlinkPattern(-midiSettings.octave);
+
+ strip.setPixelColor(11, blinkOctave ? colorConfig.octDnColor : LEDOFF);
+ strip.setPixelColor(26, colorConfig.octUpColor);
+ }
+ }
+ else if (chords_[selectedChord_].type == CTYPE_INTERVAL)
+ {
+ // Function Keys
+ if (funcKeyMode_ == FUNCKEYMODE_F3)
+ {
+ auto f3Color = blinkState ? LEDOFF : FUNKTHREE;
+ strip.setPixelColor(1, f3Color);
+ strip.setPixelColor(2, f3Color);
+ }
+ else
+ {
+ auto f1Color = (funcKeyMode_ == FUNCKEYMODE_F1 && blinkState) ? LEDOFF : FUNKONE;
+ strip.setPixelColor(1, f1Color);
+
+ auto f2Color = (funcKeyMode_ == FUNCKEYMODE_F2 && blinkState) ? LEDOFF : FUNKTWO;
+ strip.setPixelColor(2, f2Color);
+ }
+
+ strip.setPixelColor(3, kOctaveColor); // Octave
+ strip.setPixelColor(4, kTransposeColor); // Transpose
+ strip.setPixelColor(5, kSpreadColor); // Spread
+ strip.setPixelColor(6, kRotateColor); // Rotate
+ strip.setPixelColor(7, kVoicingColor); // Voicing
+ strip.setPixelColor(10, ROSE); // Show Chord Notes
+
+ if (chordEditParam_ == 0)
+ {
+ // Num Notes
+ for (uint8_t i = 11; i < 15; i++)
+ {
+ auto numNotesColor = chords_[selectedChord_].numNotes == (i - 11) + 1 ? kNumNotesSelColor : kNumNotesColor;
+ strip.setPixelColor(i, numNotesColor);
+ }
+
+ strip.setPixelColor(15, chords_[selectedChord_].spreadUpDown ? kSpreadUpDownOnColor : kSpreadUpDownOffColor);
+ strip.setPixelColor(16, chords_[selectedChord_].quartalVoicing ? kQuartalVoicingOnColor : kQuartalVoicingOffColor);
+
+ // Degree
+ for (uint8_t i = 19; i < 27; i++)
+ {
+ strip.setPixelColor(i, chords_[selectedChord_].degree == i - 19 ? kDegreeSelColor : kDegreeColor);
+ }
+ }
+ else if (chordEditParam_ == 1) // Octave
+ {
+ strip.setPixelColor(3, blinkState ? LEDOFF : kOctaveColor);
+
+ for (uint8_t i = 11; i < 16; i++)
+ {
+ auto valColor = chords_[selectedChord_].octave == (i - 11 - 2) ? WHITE : GREEN;
+ strip.setPixelColor(i, valColor);
+ }
+ }
+ else if (chordEditParam_ == 2) // Transpose
+ {
+ strip.setPixelColor(4, blinkState ? LEDOFF : kTransposeColor);
+
+ for (uint8_t i = 11; i < 26; i++)
+ {
+ auto valColor = chords_[selectedChord_].transpose == (i - 11 - 7) ? WHITE : GREEN;
+ strip.setPixelColor(i, valColor);
+ }
+ }
+ else if (chordEditParam_ == 3) // Spread
+ {
+ strip.setPixelColor(5, blinkState ? LEDOFF : kSpreadColor);
+
+ for (uint8_t i = 11; i < 16; i++)
+ {
+ auto valColor = chords_[selectedChord_].spread == (i - 11 - 2) ? WHITE : GREEN;
+ strip.setPixelColor(i, valColor);
+ }
+ }
+ else if (chordEditParam_ == 4) // Rotate
+ {
+ strip.setPixelColor(6, blinkState ? LEDOFF : kRotateColor);
+
+ for (uint8_t i = 11; i < 16; i++)
+ {
+ auto valColor = chords_[selectedChord_].rotate == (i - 11) ? WHITE : GREEN;
+ strip.setPixelColor(i, valColor);
+ }
+ }
+ else if (chordEditParam_ == 5) // Voicing
+ {
+ strip.setPixelColor(7, blinkState ? LEDOFF : kVoicingColor);
+
+ for (uint8_t i = 11; i < 19; i++)
+ {
+ auto valColor = chords_[selectedChord_].voicing == (i - 11) ? WHITE : GREEN;
+ strip.setPixelColor(i, valColor);
+ }
+ }
+ }
+
+ if (isSubmodeEnabled())
+ {
+ bool blinkStateSlow = omxLeds.getSlowBlinkState();
+ auto auxColor = (blinkStateSlow ? RED : LEDOFF);
+ strip.setPixelColor(0, auxColor);
+ }
+}
+
+void OmxModeChords::setupPageLegend(uint8_t index, uint8_t paramType)
+{
+ switch (paramType)
+ {
+ case CPARAM_UIMODE:
+ {
+ omxDisp.legends[index] = "UI";
+ omxDisp.legendText[index] = kUIModeDisp[uiMode_];
+ }
+ break;
+ case CPARAM_MAN_STRUM:
+ {
+ omxDisp.legends[index] = "STRUM";
+ omxDisp.legendText[index] = mode_ == CHRDMODE_MANSTRUM ? "ON" : "OFF";
+ }
+ break;
+ // case CPARAM_CHORD_TYPE:
+ // {
+ // omxDisp.legends[index] = "TYPE";
+ // omxDisp.legendText[index] = kChordTypeDisp[chords_[selectedChord_].type];
+ // }
+ // break;
+ // case CPARAM_CHORD_MFX:
+ // {
+ // omxDisp.legends[index] = "MIFX";
+ // if (chords_[selectedChord_].midiFx >= 0)
+ // {
+ // omxDisp.legendVals[index] = chords_[selectedChord_].midiFx + 1;
+ // }
+ // else
+ // {
+ // omxDisp.legendText[index] = "OFF";
+ // }
+ // }
+ // break;
+ // case CPARAM_CHORD_VEL:
+ // {
+ // omxDisp.legends[index] = "VEL";
+ // omxDisp.legendVals[index] = chords_[selectedChord_].velocity;
+ // }
+ // break;
+ // case CPARAM_CHORD_MCHAN:
+ // {
+ // omxDisp.legends[index] = "MCHAN";
+ // omxDisp.legendVals[index] = chords_[selectedChord_].mchan + 1;
+ // }
+ // break;
+ // case CPARAM_BAS_NOTE:
+ // {
+ // omxDisp.legends[index] = "NOTE";
+ // omxDisp.legendText[index] = MusicScales::getNoteName(chords_[selectedChord_].note);
+ // }
+ // break;
+ // case CPARAM_BAS_OCT:
+ // {
+ // omxDisp.legends[index] = "C-OCT";
+ // omxDisp.legendVals[index] = chords_[selectedChord_].basicOct + 4;
+ // }
+ // break;
+ // case CPARAM_BAS_CHORD:
+ // {
+ // omxDisp.legends[index] = "CHRD";
+ // omxDisp.legendVals[index] = chords_[selectedChord_].chord;
+ // }
+ // break;
+ // case CPARAM_BAS_BALANCE:
+ // {
+ // omxDisp.legends[index] = "BAL";
+ // omxDisp.legendVals[index] = map(chords_[selectedChord_].balance, 0, (kNumChordBalance - 1) * 10, 0, 127);
+ // }
+ // break;
+ // case CPARAM_INT_NUMNOTES:
+ // {
+ // omxDisp.legends[index] = "#NTS";
+ // omxDisp.legendVals[index] = chords_[selectedChord_].numNotes;
+ // }
+ // break;
+ // case CPARAM_INT_DEGREE:
+ // {
+ // omxDisp.legends[index] = "DEG";
+ // omxDisp.legendVals[index] = chords_[selectedChord_].degree;
+ // }
+ // break;
+ // case CPARAM_INT_OCTAVE:
+ // {
+ // omxDisp.legends[index] = "OCT";
+ // omxDisp.legendVals[index] = chords_[selectedChord_].octave;
+ // }
+ // break;
+ // case CPARAM_INT_TRANSPOSE:
+ // {
+ // omxDisp.legends[index] = "TPS";
+ // omxDisp.legendVals[index] = chords_[selectedChord_].transpose;
+ // }
+ // break;
+ // case CPARAM_INT_SPREAD:
+ // {
+ // omxDisp.legends[index] = "SPRD";
+ // omxDisp.legendVals[index] = chords_[selectedChord_].spread;
+ // }
+ // break;
+ // case CPARAM_INT_ROTATE:
+ // {
+ // omxDisp.legends[index] = "ROT";
+ // omxDisp.legendVals[index] = chords_[selectedChord_].rotate;
+ // }
+ // break;
+ // case CPARAM_INT_VOICING:
+ // {
+ // omxDisp.legends[index] = "VOIC";
+ // omxDisp.legendText[index] = kVoicingNames[chords_[selectedChord_].voicing];
+ // }
+ // break;
+ // case CPARAM_INT_SPRDUPDOWN:
+ // {
+ // omxDisp.legends[index] = "UPDN";
+ // omxDisp.legendText[index] = chords_[selectedChord_].spreadUpDown ? "ON" : "OFF";
+ // }
+ // break;
+ // case CPARAM_INT_QUARTVOICE:
+ // {
+ // omxDisp.legends[index] = "QRTV";
+ // omxDisp.legendText[index] = chords_[selectedChord_].quartalVoicing ? "ON" : "OFF";
+ // }
+ // break;
+ }
+}
+
+void OmxModeChords::setupPageLegends()
+{
+ omxDisp.clearLegends();
+
+ int8_t page = getParams()->getSelPage();
+
+ auto chordPtr = &chords_[selectedChord_];
+
+ switch (page)
+ {
+ case CHRDPAGE_GBL1:
+ {
+ setupPageLegend(0, CPARAM_UIMODE);
+ }
+ break;
+ case CHRDPAGE_OUTMIDI:
+ {
+ omxUtil.setupPageLegend(0, GPARAM_MOUT_OCT);
+ omxUtil.setupPageLegend(1, GPARAM_MOUT_CHAN);
+ omxUtil.setupPageLegend(2, GPARAM_MOUT_VEL);
+ }
+ break;
+ case CHRDPAGE_POTSANDMACROS:
+ {
+ omxUtil.setupPageLegend(0, GPARAM_POTS_PBANK);
+ omxUtil.setupPageLegend(1, GPARAM_MIDI_THRU);
+ omxUtil.setupPageLegend(2, GPARAM_MACRO_MODE);
+ omxUtil.setupPageLegend(3, GPARAM_MACRO_CHAN);
+ }
+ break;
+ case CHRDPAGE_SCALES:
+ {
+ omxUtil.setupPageLegend(musicScale_, 0, GPARAM_SCALE_ROOT);
+ omxUtil.setupPageLegend(musicScale_, 1, GPARAM_SCALE_PAT);
+ omxUtil.setupPageLegend(musicScale_, 2, GPARAM_SCALE_LOCK);
+ omxUtil.setupPageLegend(musicScale_, 3, GPARAM_SCALE_GRP16);
+ }
+ break;
+ case CHRDPAGE_1:
+ {
+ chordUtil.setupPageLegend(chordPtr, 0, CPARAM_CHORD_TYPE);
+ chordUtil.setupPageLegend(chordPtr, 1, CPARAM_CHORD_MFX);
+ chordUtil.setupPageLegend(chordPtr, 2, CPARAM_CHORD_VEL);
+ chordUtil.setupPageLegend(chordPtr, 3, CPARAM_CHORD_MCHAN);
+ }
+ break;
+ case CHRDPAGE_2:
+ {
+ if (chords_[selectedChord_].type == CTYPE_INTERVAL)
+ {
+ chordUtil.setupPageLegend(chordPtr, 0, CPARAM_INT_NUMNOTES);
+ chordUtil.setupPageLegend(chordPtr, 1, CPARAM_INT_DEGREE);
+ chordUtil.setupPageLegend(chordPtr, 2, CPARAM_INT_OCTAVE);
+ chordUtil.setupPageLegend(chordPtr, 3, CPARAM_INT_TRANSPOSE);
+ }
+ else if (chords_[selectedChord_].type == CTYPE_BASIC)
+ {
+ chordUtil.setupPageLegend(chordPtr, 0, CPARAM_BAS_NOTE);
+ chordUtil.setupPageLegend(chordPtr, 1, CPARAM_BAS_OCT);
+ chordUtil.setupPageLegend(chordPtr, 2, CPARAM_BAS_CHORD);
+ chordUtil.setupPageLegend(chordPtr, 3, CPARAM_BAS_BALANCE);
+ }
+ }
+ break;
+ case CHRDPAGE_3:
+ {
+ if (chords_[selectedChord_].type == CTYPE_INTERVAL)
+ {
+ chordUtil.setupPageLegend(chordPtr, 0, CPARAM_INT_SPREAD);
+ chordUtil.setupPageLegend(chordPtr, 1, CPARAM_INT_ROTATE);
+ chordUtil.setupPageLegend(chordPtr, 2, CPARAM_INT_VOICING);
+ }
+ }
+ break;
+ case CHRDPAGE_4:
+ {
+ if (chords_[selectedChord_].type == CTYPE_INTERVAL)
+ {
+ chordUtil.setupPageLegend(chordPtr, 0, CPARAM_INT_SPRDUPDOWN);
+ chordUtil.setupPageLegend(chordPtr, 1, CPARAM_INT_QUARTVOICE);
+ }
+ }
+ break;
+ default:
+ break;
+ }
+}
+
+void OmxModeChords::onDisplayUpdate()
+{
+ // omxLeds.updateBlinkStates();
+
+ if (isSubmodeEnabled())
+ {
+ if (omxLeds.isDirty())
+ {
+ updateLEDs();
+ }
+
+ activeSubmode->onDisplayUpdate();
+ return;
+ }
+
+ bool macroConsumesDisplay = false;
+
+ if (macroActive_ && activeMacro_ != nullptr)
+ {
+ activeMacro_->drawLEDs();
+ macroConsumesDisplay = activeMacro_->consumesDisplay();
+ }
+ else
+ {
+ if (omxLeds.isDirty())
+ {
+ updateLEDs();
+ }
+ }
+
+ if (macroConsumesDisplay)
+ {
+ activeMacro_->onDisplayUpdate();
+ }
+ else
+ {
+ if (omxDisp.isDirty())
+ {
+ if (!encoderConfig.enc_edit)
+ {
+ auto params = getParams();
+
+ if (chordEditMode_ == false && (mode_ == CHRDMODE_EDIT) && funcKeyMode_ == FUNCKEYMODE_F1) // Edit mode enter edit mode
+ {
+ omxDisp.dispGenericModeLabel("Edit chord", params->getNumPages(), params->getSelPage());
+ }
+ else if (chordEditMode_ == false && (mode_ == CHRDMODE_EDIT) && funcKeyMode_ == FUNCKEYMODE_F2) // Edit mode copy
+ {
+ omxDisp.dispGenericModeLabel("Copy to", params->getNumPages(), params->getSelPage());
+ }
+ if (chordEditMode_ == false && (mode_ == CHRDMODE_PLAY || mode_ == CHRDMODE_EDIT || mode_ == CHRDMODE_MANSTRUM) && funcKeyMode_ == FUNCKEYMODE_F2) // Play mode copy
+ {
+ omxDisp.dispGenericModeLabel("Copy to", params->getNumPages(), params->getSelPage());
+ }
+ // else if (chordEditMode_ == false && (mode_ == CHRDMODE_PRESET) && funcKeyMode_ == FUNCKEYMODE_F1) // Preset move load
+ // {
+ // omxDisp.dispGenericModeLabel("Load from", params->getNumPages(), params->getSelPage());
+ // }
+ // else if (chordEditMode_ == false && (mode_ == CHRDMODE_PRESET) && funcKeyMode_ == FUNCKEYMODE_F2) // Preset move save
+ // {
+ // omxDisp.dispGenericModeLabel("Save to", params->getNumPages(), params->getSelPage());
+ // }
+ else if (chordEditMode_ == false && mode_ == CHRDMODE_MANSTRUM)
+ {
+ omxDisp.dispGenericModeLabel("Enc Strum", params->getNumPages(), 0);
+ }
+ else if (params->getSelPage() == CHRDPAGE_NOTES)
+ {
+ if (chordNotes_[selectedChord_].active || chordEditNotes_.active)
+ {
+ notesString = "";
+ // notesString2 = "";
+
+ for (uint8_t i = 0; i < 6; i++)
+ {
+ int8_t note = chordNotes_[selectedChord_].notes[i];
+
+ if (chordEditNotes_.active)
+ {
+ note = chordEditNotes_.notes[i];
+ }
+
+ if (note >= 0 && note <= 127)
+ {
+ if (i > 0)
+ {
+ notesString.append(" ");
+ }
+ notesString.append(musicScale_->getFullNoteName(note));
+
+ // if(i < 4)
+ // {
+ // if (i > 0)
+ // {
+ // notesString.append(" ");
+ // }
+ // notesString.append(musicScale_->getFullNoteName(note));
+ // }
+ // else
+ // {
+ // if (i > 4)
+ // {
+ // notesString2.append(" ");
+ // }
+ // notesString2.append(musicScale_->getFullNoteName(note));
+
+ // }
+ }
+ }
+
+ const char *labels[1];
+ labels[0] = notesString.c_str();
+ // omxDisp.dispGenericModeLabelDoubleLine(notesString.c_str(), notesString2.c_str(), params->getNumPages(), params->getSelPage());
+ if (chordEditNotes_.active)
+ {
+ // int rootNote = chords_[selectedChord_].note;
+ omxDisp.dispKeyboard(chordEditNotes_.rootNote, chordEditNotes_.notes, true, labels, 1);
+ }
+ else
+ {
+ omxDisp.dispKeyboard(chordNotes_[selectedChord_].rootNote, chordNotes_[selectedChord_].notes, true, labels, 1);
+ }
+ }
+ else
+ {
+ omxDisp.dispKeyboard(-1, noNotes, false, nullptr, 0);
+
+ // omxDisp.dispGenericModeLabel("-", params->getNumPages(), params->getSelPage());
+ }
+ }
+ // Chord page
+ else if (params->getSelPage() == CHRDPAGE_2 && chords_[selectedChord_].type == CTYPE_BASIC)
+ {
+ auto noteName = MusicScales::getNoteName(chords_[selectedChord_].note, true);
+ int octave = chords_[selectedChord_].basicOct + 4;
+ notesString2 = String(octave);
+ auto chordType = kChordMsg[chords_[selectedChord_].chord];
+
+ activeChordBalance_ = getChordBalanceDetails(chords_[selectedChord_].balance);
+
+ omxDisp.dispChordBasicPage(params->getSelParam(), getEncoderSelect(), noteName, notesString2.c_str(), chordType, activeChordBalance_.type, activeChordBalance_.velMult);
+ }
+ // Custom Chord Notes
+ else if (params->getSelPage() == CHRDPAGE_3 && chords_[selectedChord_].type == CTYPE_BASIC && chords_[selectedChord_].chord == kCustomChordPattern)
+ {
+ const char *labels[6];
+ const char *headers[1];
+ headers[0] = "Custom Chord";
+
+ for (uint8_t i = 0; i < 6; i++)
+ {
+ int note = chords_[selectedChord_].customNotes[i].note;
+
+ if (note == 0)
+ {
+ if (i == 0)
+ {
+ customNotesStrings[i] = "RT";
+ }
+ else
+ {
+ customNotesStrings[i] = "-";
+ }
+ }
+ else
+ {
+ if (note > 0)
+ {
+ customNotesStrings[i] = "+" + String(note);
+ }
+ else
+ {
+ customNotesStrings[i] = "" + String(note);
+ }
+ }
+
+ labels[i] = customNotesStrings[i].c_str();
+ }
+
+ omxDisp.dispCenteredSlots(labels, 6, params->getSelParam(), getEncoderSelect(), true, true, headers, 1);
+ }
+ else
+ {
+
+ setupPageLegends();
+ omxDisp.dispGenericMode2(params->getNumPages(), params->getSelPage(), params->getSelParam(), getEncoderSelect());
+ }
+ }
+ }
+ }
+}
+
+void OmxModeChords::SetScale(MusicScales *scale)
+{
+ musicScale_ = scale;
+}
+
+bool OmxModeChords::pasteSelectedChordTo(uint8_t chordIndex)
+{
+ if (chordIndex == selectedChord_ || chordIndex >= 16)
+ return false;
+
+ chords_[chordIndex].CopySettingsFrom(&chords_[selectedChord_]);
+ selectedChord_ = chordIndex;
+ lastKeyWasKeyboard_ = false;
+ return true;
+}
+bool OmxModeChords::loadPreset(uint8_t presetIndex)
+{
+ if (presetIndex >= NUM_CHORD_SAVES)
+ return false;
+
+ for (uint8_t i = 0; i < 16; i++)
+ {
+ chords_[i].CopySettingsFrom(&chordSaves_[presetIndex][i]);
+ }
+
+ selectedSave_ = presetIndex;
+
+ return true;
+}
+
+bool OmxModeChords::savePreset(uint8_t presetIndex)
+{
+ if (presetIndex >= NUM_CHORD_SAVES)
+ return false;
+
+ for (uint8_t i = 0; i < 16; i++)
+ {
+ chordSaves_[presetIndex][i].CopySettingsFrom(&chords_[i]);
+ }
+
+ selectedSave_ = presetIndex;
+
+ return true;
+}
+
+void OmxModeChords::onManualStrumOn(uint8_t chordIndex)
+{
+ // Serial.println("onManualStrumOn: " + String(chordIndex));
+ if (chordNotes_[chordIndex].active)
+ {
+ // Serial.println("chord already active");
+ return; // This shouldn't happen
+ }
+
+ if (constructChord(chordIndex))
+ {
+ chordNotes_[chordIndex].active = true;
+ chordNotes_[chordIndex].channel = sysSettings.midiChannel;
+ // uint8_t velocity = midiSettings.defaultVelocity;
+
+ chordNotes_[chordIndex].strumPos = 0;
+ chordNotes_[chordIndex].encDelta = 0;
+ chordNotes_[chordIndex].octIncrement = 0;
+
+ // Serial.print("Chord: ");
+ // for(uint8_t i = 0; i < 6; i++)
+ // {
+ // int note = chordNotes_[chordIndex].notes[i];
+ // // Serial.print(String(note) + " ");
+ // if(note >= 0 && note <= 127)
+ // {
+ // MM::sendNoteOn(note, velocity, chordNotes_[chordIndex].channel);
+ // }
+ // }
+ // Serial.print("\n");
+ }
+ else
+ {
+ Serial.println("constructChord failed");
+ }
+}
+
+void OmxModeChords::onChordOn(uint8_t chordIndex)
+{
+ // Serial.println("onChordOn: " + String(chordIndex));
+ if (chordNotes_[chordIndex].active)
+ {
+ // Serial.println("chord already active");
+ return; // This shouldn't happen
+ }
+
+ if (constructChord(chordIndex))
+ {
+ chordNotes_[chordIndex].active = true;
+ chordNotes_[chordIndex].channel = chords_[chordIndex].mchan + 1;
+
+ // Prevent stuck notes
+ playedChordNotes_[chordIndex].CopyFrom(&chordNotes_[chordIndex]);
+ // uint8_t velocity = chords_[chordIndex].velocity;
+
+ // uint32_t noteOnMicros = micros();
+
+ // Serial.print("Chord: ");
+ for (uint8_t i = 0; i < 6; i++)
+ {
+ int note = chordNotes_[chordIndex].notes[i];
+ uint8_t velocity = chordNotes_[chordIndex].velocities[i];
+
+ // Serial.print("Note: " + String(note));
+ // Serial.print(" Vel: " + String(velocity));
+ // Serial.print("\n");
+
+ // if(note >= 0 && note <= 127)
+ // {
+ // // MM::sendNoteOn(note, velocity, chordNotes_[chordIndex].channel);
+ // pendingNoteOns.insert(note, velocity, chordNotes_[chordIndex].channel, noteOnMicros, false);
+ // }
+
+ doNoteOn(note, chordNotes_[chordIndex].midifx, velocity, chordNotes_[chordIndex].channel);
+ }
+ // Serial.print("\n");
+ }
+ else
+ {
+ // Serial.println("constructChord failed");
+ }
+}
+
+void OmxModeChords::onChordOff(uint8_t chordIndex)
+{
+ // Serial.println("onChordOff: " + String(chordIndex));
+ if (chordNotes_[chordIndex].active == false)
+ return;
+
+ for (uint8_t i = 0; i < 6; i++)
+ {
+ int note = playedChordNotes_[chordIndex].notes[i];
+
+ doNoteOff(note, playedChordNotes_[chordIndex].midifx, playedChordNotes_[chordIndex].channel);
+
+ // if (note >= 0 && note <= 127)
+ // {
+ // // MM::sendNoteOff(note, 0, chordNotes_[chordIndex].channel);
+
+ // pendingNoteOns.remove(note, chordNotes_[chordIndex].channel);
+ // pendingNoteOffs.sendOffNow(note, chordNotes_[chordIndex].channel, false);
+ // }
+ }
+ chordNotes_[chordIndex].active = false;
+}
+
+void OmxModeChords::onChordEditOn(uint8_t chordIndex)
+{
+ // Serial.println("onChordOn: " + String(chordIndex));
+ if (chordEditNotes_.active)
+ {
+ // Serial.println("chord already active");
+ return; // This shouldn't happen
+ }
+
+ if (constructChord(chordIndex))
+ {
+ // chordNotes_[chordIndex].active = true;
+ chordNotes_[chordIndex].channel = chords_[chordIndex].mchan + 1;
+ // uint8_t velocity = chords_[chordIndex].velocity;
+
+ chordEditNotes_.CopyFrom(&chordNotes_[chordIndex]);
+ chordEditNotes_.active = true;
+
+ // chordEditNotes_.channel = chordNotes_[chordIndex].channel;
+ // chordEditNotes_.rootNote = chordNotes_[chordIndex].rootNote;
+
+ // uint32_t noteOnMicros = micros();
+
+ // Serial.print("Chord: ");
+ for (uint8_t i = 0; i < 6; i++)
+ {
+ int note = chordEditNotes_.notes[i];
+ uint8_t velocity = chordEditNotes_.velocities[i];
+
+ // chordEditNotes_.notes[i] = note;
+ // Serial.print(String(note) + " ");
+ // if(note >= 0 && note <= 127)
+ // {
+ // // MM::sendNoteOn(note, velocity, chordNotes_[chordIndex].channel);
+ // pendingNoteOns.insert(note, velocity, chordNotes_[chordIndex].channel, noteOnMicros, false);
+ // }
+
+ doNoteOn(note, chordEditNotes_.midifx, velocity, chordEditNotes_.channel);
+ }
+ // Serial.print("\n");
+ }
+ else
+ {
+ // Serial.println("constructChord failed");
+ }
+}
+
+void OmxModeChords::onChordEditOff()
+{
+ // onChordOff(selectedChord_);
+
+ // Serial.println("onChordOff: " + String(chordIndex));
+ if (chordEditNotes_.active == false)
+ return;
+
+ for (uint8_t i = 0; i < 6; i++)
+ {
+ int note = chordEditNotes_.notes[i];
+
+ doNoteOff(note, chordEditNotes_.midifx, chordEditNotes_.channel);
+
+ // if (note >= 0 && note <= 127)
+ // {
+ // // MM::sendNoteOff(note, 0, chordNotes_[chordIndex].channel);
+
+ // pendingNoteOns.remove(note, chordNotes_[chordIndex].channel);
+ // pendingNoteOffs.sendOffNow(note, chordNotes_[chordIndex].channel, false);
+ // }
+ }
+ chordEditNotes_.active = false;
+}
+
+bool OmxModeChords::constructChord(uint8_t chordIndex)
+{
+ // Serial.println("Constructing Chord: " + String(chordIndex));
+ auto chord = chords_[chordIndex];
+
+ if (chord.type == CTYPE_BASIC)
+ {
+ return constructChordBasic(chordIndex);
+ }
+
+ int8_t octave = midiSettings.octave + chord.octave;
+
+ uint8_t numNotes = 0;
+
+ for (uint8_t i = 0; i < 6; i++)
+ {
+ chordNotes_[chordIndex].notes[i] = -1;
+ chordNotes_[chordIndex].velocities[i] = chord.velocity;
+ }
+
+ if (chord.numNotes == 0)
+ {
+ return false;
+ }
+ else if (chord.numNotes == 1)
+ {
+ chordNotes_[chordIndex].notes[0] = musicScale_->getNoteByDegree(chord.degree, octave);
+ numNotes = 1;
+ }
+ else if (chord.numNotes == 2)
+ {
+ chordNotes_[chordIndex].notes[0] = musicScale_->getNoteByDegree(chord.degree, octave);
+ chordNotes_[chordIndex].notes[1] = musicScale_->getNoteByDegree(chord.degree + 2, octave);
+ numNotes = 2;
+ }
+ else if (chord.numNotes == 3)
+ {
+ chordNotes_[chordIndex].notes[0] = musicScale_->getNoteByDegree(chord.degree, octave);
+ chordNotes_[chordIndex].notes[1] = musicScale_->getNoteByDegree(chord.degree + 2, octave);
+ chordNotes_[chordIndex].notes[2] = musicScale_->getNoteByDegree(chord.degree + 4, octave);
+ numNotes = 3;
+ }
+ else if (chord.numNotes == 4)
+ {
+ chordNotes_[chordIndex].notes[0] = musicScale_->getNoteByDegree(chord.degree, octave);
+ chordNotes_[chordIndex].notes[1] = musicScale_->getNoteByDegree(chord.degree + 2, octave);
+ chordNotes_[chordIndex].notes[2] = musicScale_->getNoteByDegree(chord.degree + 4, octave);
+ chordNotes_[chordIndex].notes[3] = musicScale_->getNoteByDegree(chord.degree + 6, octave);
+ numNotes = 4;
+ }
+
+ chordNotes_[chordIndex].rootNote = chordNotes_[chordIndex].notes[0];
+
+ // Serial.println("numNotes: " + String(numNotes));
+
+ switch (chord.voicing)
+ {
+ case CHRDVOICE_NONE:
+ {
+ }
+ break;
+ case CHRDVOICE_POWER:
+ {
+ if (chord.numNotes > 1)
+ {
+ chordNotes_[chordIndex].notes[1] = musicScale_->getNoteByDegree(chord.degree + 4, octave);
+ }
+ if (chord.numNotes > 2)
+ {
+ chordNotes_[chordIndex].notes[2] = chordNotes_[chordIndex].notes[1] + 12;
+ for (uint8_t i = 3; i < 6; i++)
+ {
+ chordNotes_[chordIndex].notes[i] = -1;
+ }
+ numNotes = 3;
+ }
+ }
+ break;
+ case CHRDVOICE_SUS2:
+ {
+ if (chord.numNotes > 1)
+ {
+ chordNotes_[chordIndex].notes[1] = musicScale_->getNoteByDegree(chord.degree + 1, octave);
+ }
+ }
+ break;
+ case CHRDVOICE_SUS4:
+ {
+ if (chord.numNotes > 1)
+ {
+ chordNotes_[chordIndex].notes[1] = musicScale_->getNoteByDegree(chord.degree + 3, octave);
+ }
+ }
+ break;
+ case CHRDVOICE_SUS24:
+ {
+ if (chord.numNotes > 1)
+ {
+ chordNotes_[chordIndex].notes[1] = musicScale_->getNoteByDegree(chord.degree + 1, octave);
+ }
+ if (chord.numNotes > 2)
+ {
+ chordNotes_[chordIndex].notes[2] = musicScale_->getNoteByDegree(chord.degree + 3, octave);
+ }
+ }
+ break;
+ case CHRDVOICE_ADD6:
+ {
+ chordNotes_[chordIndex].notes[chord.numNotes] = musicScale_->getNoteByDegree(chord.degree + 5, octave);
+ numNotes = chord.numNotes + 1;
+ }
+ break;
+ case CHRDVOICE_ADD69:
+ {
+ chordNotes_[chordIndex].notes[chord.numNotes] = musicScale_->getNoteByDegree(chord.degree + 5, octave);
+ chordNotes_[chordIndex].notes[chord.numNotes + 1] = musicScale_->getNoteByDegree(chord.degree + 8, octave);
+ numNotes = chord.numNotes + 2;
+ }
+ break;
+ case CHRDVOICE_KB11:
+ {
+ if (chord.numNotes > 1)
+ {
+ chordNotes_[chordIndex].notes[0] = musicScale_->getNoteByDegree(chord.degree + 0, octave);
+ chordNotes_[chordIndex].notes[1] = musicScale_->getNoteByDegree(chord.degree + 4, octave);
+ numNotes = 2;
+ }
+ if (chord.numNotes > 2)
+ {
+ chordNotes_[chordIndex].notes[2] = musicScale_->getNoteByDegree(chord.degree + 8, octave);
+ numNotes = 3;
+ }
+ if (chord.numNotes > 3)
+ {
+ chordNotes_[chordIndex].notes[3] = musicScale_->getNoteByDegree(chord.degree + 9, octave);
+ chordNotes_[chordIndex].notes[4] = musicScale_->getNoteByDegree(chord.degree + 6, octave + 1);
+ chordNotes_[chordIndex].notes[5] = musicScale_->getNoteByDegree(chord.degree + 10, octave + 1);
+ numNotes = 6;
+ }
+ }
+ break;
+ default:
+ break;
+ }
+
+ // Serial.println("numNotes: " + String(numNotes));
+
+ if (chord.quartalVoicing)
+ {
+ chordNotes_[chordIndex].notes[0] = AddOctave(chordNotes_[chordIndex].notes[0], 2);
+ chordNotes_[chordIndex].notes[1] = AddOctave(chordNotes_[chordIndex].notes[1], 0);
+ chordNotes_[chordIndex].notes[2] = AddOctave(chordNotes_[chordIndex].notes[2], 1);
+ chordNotes_[chordIndex].notes[3] = AddOctave(chordNotes_[chordIndex].notes[3], -1);
+ }
+
+ if (chord.spreadUpDown)
+ {
+ for (uint8_t i = 0; i < 6; i++)
+ {
+ if (i % 2 == 0)
+ {
+ chordNotes_[chordIndex].notes[i] = AddOctave(chordNotes_[chordIndex].notes[i], -1);
+ }
+ else
+ {
+ chordNotes_[chordIndex].notes[i] = AddOctave(chordNotes_[chordIndex].notes[i], 1);
+ }
+ }
+ }
+
+ if (chord.spread < 0)
+ {
+ for (uint8_t i = 0; i < 6; i++)
+ {
+ if (i % 2 == 0)
+ {
+ chordNotes_[chordIndex].notes[i] = AddOctave(chordNotes_[chordIndex].notes[i], chord.spread);
+ }
+ }
+ }
+ else if (chord.spread > 0)
+ {
+ for (uint8_t i = 0; i < 6; i++)
+ {
+ if (i % 2 != 0)
+ {
+ chordNotes_[chordIndex].notes[i] = AddOctave(chordNotes_[chordIndex].notes[i], chord.spread);
+ }
+ }
+ }
+
+ if (chord.rotate != 0 && numNotes > 0)
+ {
+ int temp[numNotes];
+
+ uint8_t val = numNotes - chord.rotate;
+
+ uint8_t offset = chord.rotate % numNotes;
+
+ for (uint8_t i = 0; i < offset; i++)
+ {
+ chordNotes_[chordIndex].notes[i] = AddOctave(chordNotes_[chordIndex].notes[i], 1);
+ }
+
+ for (uint8_t i = 0; i < numNotes; i++)
+ {
+ temp[i] = chordNotes_[chordIndex].notes[abs((i + val) % numNotes)];
+ }
+ for (int i = 0; i < numNotes; i++)
+ {
+ chordNotes_[chordIndex].notes[i] = temp[i];
+ }
+ }
+
+ for (uint8_t i = 0; i < 6; i++)
+ {
+ chordNotes_[chordIndex].notes[i] = TransposeNote(chordNotes_[chordIndex].notes[i], chord.transpose);
+ }
+
+ chordNotes_[chordIndex].midifx = chord.midiFx;
+
+ return true;
+}
+
+bool OmxModeChords::constructChordBasic(uint8_t chordIndex)
+{
+ auto chord = chords_[chordIndex];
+
+ // int8_t octave = midiSettings.octave + chord.octave;
+
+ // uint8_t numNotes = 0;
+
+ for (uint8_t i = 0; i < 6; i++)
+ {
+ chordNotes_[chordIndex].notes[i] = -1;
+ }
+
+ // int adjRoot = notes[thisKey] + (midiSettings.octave + 1 * 12);
+
+ int rootNote = chord.note + ((chord.basicOct + 5) * 12);
+
+ if (rootNote < 0 || rootNote > 127)
+ return false;
+
+ chordNotes_[chordIndex].rootNote = rootNote;
+
+ chordNotes_[chordIndex].midifx = chord.midiFx;
+
+ chordNotes_[chordIndex].notes[0] = rootNote;
+
+ if (chord.chord == kCustomChordPattern)
+ {
+ for (uint8_t i = 0; i < 6; i++)
+ {
+ int noteOffset = chord.customNotes[i].note;
+
+ if (noteOffset != 0 || (noteOffset == 0 && i == 0))
+ {
+ chordNotes_[chordIndex].notes[i] = rootNote + noteOffset;
+ }
+ // else offset is zero, do nothing.
+ }
+ }
+ else
+ {
+ auto pattern = chordPatterns[chord.chord];
+
+ for (uint8_t i = 0; i < 3; i++)
+ {
+ if (pattern[i] >= 0)
+ {
+ chordNotes_[chordIndex].notes[i + 1] = rootNote + pattern[i];
+ }
+ }
+ }
+
+ activeChordBalance_ = getChordBalanceDetails(chord.balance);
+
+ for (uint8_t i = 0; i < 4; i++)
+ {
+ int pnote = chordNotes_[chordIndex].notes[i];
+
+ if (pnote >= 0 && pnote <= 127)
+ {
+ int bal = activeChordBalance_.type[i];
+
+ chordNotes_[chordIndex].notes[i] = (bal <= -10 ? -1 : (pnote + (12 * bal)));
+ chordNotes_[chordIndex].velocities[i] = chord.velocity * activeChordBalance_.velMult[i];
+ }
+ }
+
+ return true;
+}
+
+ChordBalanceDetails OmxModeChords::getChordBalanceDetails(uint8_t balance)
+{
+ ChordBalanceDetails bDetails;
+
+ bDetails.type[0] = 0;
+ bDetails.velMult[0] = 1.0f;
+
+ uint8_t balanceIndex = balance / 10;
+
+ auto balancePat = chordBalance[balanceIndex];
+
+ for (uint8_t i = 0; i < 3; i++)
+ {
+ int8_t bal = balancePat[i];
+
+ bDetails.type[i + 1] = bal;
+
+ if (balanceIndex < kNumChordBalance)
+ {
+ int8_t nextBal = chordBalance[balanceIndex + 1][i];
+
+ if ((balance % 10) != 0)
+ {
+ if (nextBal > -10)
+ {
+ bDetails.type[i + 1] = nextBal;
+ }
+ }
+
+ float v1 = bal <= -10 ? 0.0f : 1.0f;
+ float v2 = nextBal <= -10 ? 0.0f : 1.0f;
+
+ bDetails.velMult[i + 1] = map((float)balance, balanceIndex * 10.0f, (balanceIndex + 1) * 10.0f, v1, v2);
+ }
+ else
+ {
+ bDetails.velMult[i + 1] = 1.0f;
+ }
+ }
+
+ return bDetails;
+}
+
+int OmxModeChords::AddOctave(int note, int8_t octave)
+{
+ if (note < 0 || note > 127)
+ return -1;
+
+ int newNote = note + (12 * octave);
+ if (newNote < 0 || newNote > 127)
+ return -1;
+ return newNote;
+}
+
+int OmxModeChords::TransposeNote(int note, int8_t semitones)
+{
+ if (note < 0 || note > 127)
+ return -1;
+
+ int newNote = note + semitones;
+ if (newNote < 0 || newNote > 127)
+ return -1;
+ return newNote;
+}
+
+int OmxModeChords::saveToDisk(int startingAddress, Storage *storage)
+{
+ int saveSize = sizeof(ChordSettings);
+
+ for (uint8_t saveIndex = 0; saveIndex < NUM_CHORD_SAVES; saveIndex++)
+ {
+ for (uint8_t i = 0; i < 16; i++)
+ {
+ auto saveBytesPtr = (byte *)(&chordSaves_[saveIndex][i]);
+ for (int j = 0; j < saveSize; j++)
+ {
+ storage->write(startingAddress + j, *saveBytesPtr++);
+ }
+
+ startingAddress += saveSize;
+ }
+ }
+
+ return startingAddress;
+}
+
+int OmxModeChords::loadFromDisk(int startingAddress, Storage *storage)
+{
+ int saveSize = sizeof(ChordSettings);
+
+ for (uint8_t saveIndex = 0; saveIndex < NUM_CHORD_SAVES; saveIndex++)
+ {
+ for (uint8_t i = 0; i < 16; i++)
+ {
+ auto chord = ChordSettings{};
+ auto current = (byte *)&chord;
+ for (int j = 0; j < saveSize; j++)
+ {
+ *current = storage->read(startingAddress + j);
+ current++;
+ }
+
+ chordSaves_[saveIndex][i].CopySettingsFrom(&chord);
+ startingAddress += saveSize;
+ }
+ }
+
+ loadPreset(0);
+
+ return startingAddress;
+}
diff --git a/Archive/OMX-27-firmware/src/modes/omx_mode_chords.h b/Archive/OMX-27-firmware/src/modes/omx_mode_chords.h
new file mode 100644
index 00000000..c6a521d0
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/modes/omx_mode_chords.h
@@ -0,0 +1,245 @@
+#pragma once
+#include "../modes/omx_mode_interface.h"
+#include "../utils/music_scales.h"
+// #include "../consts/colors.h"
+#include "../config.h"
+// #include "../modes/omx_mode_midi_keyboard.h"
+#include "../utils/param_manager.h"
+#include "../hardware/storage.h"
+#include "submodes/submode_interface.h"
+#include "submodes/submode_midifxgroup.h"
+#include "submodes/submode_preset.h"
+#include "../midimacro/midimacro_m8.h"
+#include "../midimacro/midimacro_norns.h"
+#include "../midimacro/midimacro_deluge.h"
+#include "../utils/chord_structs.h"
+#include "../utils/chord_util.h"
+
+#define NUM_CHORD_SAVES 8
+
+// struct CustomChordDegree
+// {
+// uint8_t note : 3; // 0 - 7
+// int8_t octave : 3; // Octave Offset -3 to +3
+// };
+
+class OmxModeChords : public OmxModeInterface
+{
+public:
+ OmxModeChords();
+ ~OmxModeChords() {}
+
+ void InitSetup() override;
+
+ void onModeActivated() override;
+ void onModeDeactivated() override;
+
+ void onClockTick() override;
+
+ void onPotChanged(int potIndex, int prevValue, int newValue, int analogDelta) override;
+
+ void loopUpdate(Micros elapsedTime) override;
+
+ void updateLEDs() override;
+
+ void onEncoderChanged(Encoder::Update enc) override;
+ void onEncoderButtonDown() override;
+ void onEncoderButtonDownLong() override;
+
+ void inMidiControlChange(byte channel, byte control, byte value) override;
+
+
+ bool shouldBlockEncEdit() override;
+
+ void onKeyUpdate(OMXKeypadEvent e) override;
+ void onKeyHeldUpdate(OMXKeypadEvent e) override;
+
+ void onDisplayUpdate() override;
+ void SetScale(MusicScales *scale);
+
+ int saveToDisk(int startingAddress, Storage *storage);
+ int loadFromDisk(int startingAddress, Storage *storage);
+
+private:
+ struct NoteTracker
+ {
+ int8_t triggerCount;
+ uint8_t noteNumber : 7;
+ uint8_t midiChannel : 4;
+ };
+
+ SubModePreset presetManager;
+
+ // void savePreset(uint8_t saveIndex);
+ // void loadPreset(uint8_t loadIndex);
+
+ static void doSavePresetForwarder(void *context, uint8_t presetIndex)
+ {
+ static_cast(context)->savePreset(presetIndex);
+ }
+
+ static void doLoadPresetForwarder(void *context, uint8_t presetIndex)
+ {
+ static_cast(context)->loadPreset(presetIndex);
+ }
+
+ bool macroActive_ = false;
+
+ midimacro::MidiMacroNorns nornsMarco_;
+ midimacro::MidiMacroM8 m8Macro_;
+ midimacro::MidiMacroDeluge delugeMacro_;
+
+ midimacro::MidiMacroInterface *activeMacro_;
+ midimacro::MidiMacroInterface *getActiveMacro();
+
+ // Used by the Macros for playing normal notes
+ static void doNoteOnForwarder(void *context, uint8_t keyIndex)
+ {
+ auto chordsInstance = static_cast(context);
+ chordsInstance->doNoteOn(keyIndex, chordsInstance->mfxIndex_, midiSettings.defaultVelocity, sysSettings.midiChannel);
+ }
+
+ // Used by the Macros for playing normal notes
+ static void doNoteOffForwarder(void *context, uint8_t keyIndex)
+ {
+ auto chordsInstance = static_cast(context);
+ chordsInstance->doNoteOff(keyIndex, chordsInstance->mfxIndex_, sysSettings.midiChannel);
+ }
+
+ // If true, encoder selects param rather than modifies value
+ bool auxDown_ = false;
+ bool encoderSelect_ = false;
+ bool chordEditMode_ = false;
+ // bool splitKeyboardMode_ = false;
+
+ bool mfxQuickEdit_ = false;
+ uint8_t quickEditMfxIndex_ = 0;
+
+ bool wrapManStrum_ = true;
+
+ bool lastKeyWasKeyboard_ = false; // This gets set to true if the last key pressed was a keyboard key and not a chord key
+
+ uint8_t incrementManStrum_ = 0;
+ uint8_t manStrumSensit_ = 10;
+
+ uint8_t selectedChord_ = 0;
+ int8_t heldChord_ = -1;
+
+ uint8_t selectedSave_ = 0;
+
+ uint8_t uiMode_ = 0; // FULL, Split
+
+ uint8_t mode_ = 0; // Play, Edit Chord, Presets, Manual Strum
+
+ uint8_t manStrumNoteLength_ = 4;
+
+ // ParamManager params_;
+ ParamManager basicParams_;
+ ParamManager intervalParams_;
+
+ // ParamManager chordEditParams_;
+ uint8_t funcKeyMode_ = 0;
+ uint8_t chordEditParam_ = 0; // None, Octave, Transpose, Spread, Rotate, Voicing
+
+ MusicScales *musicScale_;
+
+ ChordSettings chords_[16];
+ ChordNotes chordNotes_[16];
+
+ ChordNotes playedChordNotes_[16];
+
+ ChordNotes chordEditNotes_;
+ int8_t activeChordEditDegree_;
+ int8_t activeChordEditNoteKey_;
+ int8_t activeSplitKeyIndex_;
+
+ ChordBalanceDetails activeChordBalance_;
+
+ ChordSettings chordSaves_[NUM_CHORD_SAVES][16];
+
+ // int saveSize = sizeof(chordSaves_);
+
+ String notesString = "";
+ String notesString2 = "";
+
+ String customNotesStrings[6];
+
+ // SubModes
+ SubmodeInterface *activeSubmode = nullptr;
+
+ uint8_t mfxIndex_ = 0;
+
+ bool lockScaleCache_ = false; // Cache value when entering mode, restore on exit
+ bool grp16ScaleCache_ = false;
+
+ int noNotes[6] = {-1, -1, -1, -1, -1, -1};
+
+ const uint8_t kMaxNoteTrackerSize = 32;
+
+ std::vector noteOffTracker;
+
+ // int chordSize = sizeof(chords_);
+
+ ParamManager *getParams();
+ void setSelPageAndParam(int8_t newPage, int8_t newParam);
+
+ void allNotesOff();
+ void updateFuncKeyMode();
+ void onEncoderChangedEditParam(Encoder::Update *enc, uint8_t selectedParmIndex, uint8_t targetParamIndex, uint8_t paramType);
+ void onEncoderChangedManStrum(Encoder::Update enc);
+ void onKeyUpdateChordEdit(OMXKeypadEvent e);
+ void enterChordEditMode();
+ void updateLEDsChordEdit();
+ void setupPageLegends();
+ void setupPageLegend(uint8_t index, uint8_t paramType);
+
+ bool pasteSelectedChordTo(uint8_t chordIndex);
+ bool loadPreset(uint8_t presetIndex);
+ bool savePreset(uint8_t presetIndex);
+
+ void onManualStrumOn(uint8_t chordIndex);
+ void onChordOn(uint8_t chordIndex);
+ void onChordOff(uint8_t chordIndex);
+ void onChordEditOn(uint8_t chordIndex);
+ void onChordEditOff();
+
+ bool constructChord(uint8_t chordIndex);
+ bool constructChordBasic(uint8_t chordIndex);
+
+ static int AddOctave(int note, int8_t octave);
+ static int TransposeNote(int note, int8_t semitones);
+
+ ChordBalanceDetails getChordBalanceDetails(uint8_t balance);
+
+ void enableSubmode(SubmodeInterface *subMode);
+ void disableSubmode();
+ bool isSubmodeEnabled();
+
+ bool getEncoderSelect();
+
+ void selectMidiFx(uint8_t mfxIndex, bool dispMsg);
+ void selectMidiFxChordKey(int8_t mfxIndex, bool dispMsg);
+ bool onKeyUpdateSelMidiFX(OMXKeypadEvent e);
+ bool onKeyHeldSelMidiFX(OMXKeypadEvent e);
+
+ void doNoteOn(int noteNumber, uint8_t midifx, uint8_t velocity, uint8_t midiChannel);
+ void doNoteOff(int noteNumber, uint8_t midifx, uint8_t midiChannel);
+
+ void splitNoteOn(uint8_t keyIndex);
+ void splitNoteOff(uint8_t keyIndex);
+
+ void stopSequencers();
+
+ static void onNotePostFXForwarder(void *context, MidiNoteGroup note)
+ {
+ static_cast(context)->onNotePostFX(note);
+ }
+ void onNotePostFX(MidiNoteGroup note);
+
+ static void onPendingNoteOffForwarder(void *context, int note, int channel)
+ {
+ static_cast(context)->onPendingNoteOff(note, channel);
+ }
+
+ void onPendingNoteOff(int note, int channel);
+};
diff --git a/Archive/OMX-27-firmware/src/modes/omx_mode_drum.cpp b/Archive/OMX-27-firmware/src/modes/omx_mode_drum.cpp
new file mode 100644
index 00000000..59d16506
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/modes/omx_mode_drum.cpp
@@ -0,0 +1,1436 @@
+#include "omx_mode_drum.h"
+#include "../config.h"
+#include "../consts/colors.h"
+#include "../utils/omx_util.h"
+#include "../utils/cvNote_util.h"
+#include "../hardware/omx_disp.h"
+#include "../hardware/omx_leds.h"
+#include "../midi/midi.h"
+#include "../utils/music_scales.h"
+#include "../midi/noteoffs.h"
+
+enum DrumModePage {
+ DRUMPAGE_DRUMKEY, // Note, Chan, Vel, MidiFX
+ DRUMPAGE_DRUMKEY2, // Hue, RND Hue, Copy, Paste
+ DRUMPAGE_SCALES, // Hue,
+ DRUMPAGE_INSPECT, // Sent Pot CC, Last Note, Last Vel, Last Chan, Not editable, just FYI
+ DRUMPAGE_POTSANDMACROS, // PotBank, Thru, Macro, Macro Channel
+ DRUMPAGE_CFG,
+ DRUMPAGE_NUMPAGES
+};
+
+enum DrumEditMode {
+ DRUMMODE_NORMAL,
+ DRUMMODE_LOADKIT,
+ DRUMMODE_SAVEKIT,
+ DRUMMODE_NUM_OF_MODES,
+};
+
+OmxModeDrum::OmxModeDrum()
+{
+ params.addPages(DRUMPAGE_NUMPAGES);
+
+ m8Macro_.setDoNoteOn(&OmxModeDrum::doNoteOnForwarder, this);
+ m8Macro_.setDoNoteOff(&OmxModeDrum::doNoteOffForwarder, this);
+ nornsMarco_.setDoNoteOn(&OmxModeDrum::doNoteOnForwarder, this);
+ nornsMarco_.setDoNoteOff(&OmxModeDrum::doNoteOffForwarder, this);
+ delugeMacro_.setDoNoteOn(&OmxModeDrum::doNoteOnForwarder, this);
+ delugeMacro_.setDoNoteOff(&OmxModeDrum::doNoteOffForwarder, this);
+
+ presetManager.setContextPtr(this);
+ presetManager.setDoSaveFunc(&OmxModeDrum::doSaveKitForwarder);
+ presetManager.setDoLoadFunc(&OmxModeDrum::doLoadKitForwarder);
+}
+
+void OmxModeDrum::changeMode(uint8_t newModeIndex)
+{
+ if(newModeIndex >= DRUMMODE_NUM_OF_MODES)
+ {
+ return;
+ }
+
+ if(newModeIndex == DRUMMODE_NORMAL)
+ {
+ disableSubmode();
+ }
+ if(newModeIndex == DRUMMODE_SAVEKIT)
+ {
+ presetManager.configure(PRESETMODE_SAVE, selDrumKit, NUM_DRUM_KITS, true);
+ enableSubmode(&presetManager);
+ }
+ else if(newModeIndex == DRUMMODE_LOADKIT)
+ {
+ presetManager.configure(PRESETMODE_LOAD, selDrumKit, NUM_DRUM_KITS, true);
+ enableSubmode(&presetManager);
+ }
+}
+
+
+void OmxModeDrum::InitSetup()
+{
+ initSetup = true;
+}
+
+void OmxModeDrum::onModeActivated()
+{
+ // auto init when activated
+ if (!initSetup)
+ {
+ InitSetup();
+ }
+
+ // sequencer.playing = false;
+ stopSequencers();
+
+ omxLeds.setDirty();
+ omxDisp.setDirty();
+
+ for (uint8_t i = 0; i < NUM_MIDIFX_GROUPS; i++)
+ {
+ subModeMidiFx[i].setEnabled(true);
+ subModeMidiFx[i].onModeChanged();
+ subModeMidiFx[i].setNoteOutputFunc(&OmxModeDrum::onNotePostFXForwarder, this);
+ }
+
+ pendingNoteOffs.setNoteOffFunction(&OmxModeDrum::onPendingNoteOffForwarder, this);
+
+ params.setSelPageAndParam(0, 0);
+ encoderSelect = true;
+
+ activeDrumKit.CopyFrom(drumKits[selDrumKit]);
+
+ // selectMidiFx(mfxIndex_, false);
+}
+
+void OmxModeDrum::onModeDeactivated()
+{
+ stopSequencers();
+
+ for (uint8_t i = 0; i < NUM_MIDIFX_GROUPS; i++)
+ {
+ subModeMidiFx[i].setEnabled(false);
+ subModeMidiFx[i].onModeChanged();
+ }
+}
+
+void OmxModeDrum::stopSequencers()
+{
+ omxUtil.stopClocks();
+ pendingNoteOffs.allOff();
+}
+
+void OmxModeDrum::selectMidiFx(uint8_t mfxIndex, bool dispMsg)
+{
+ uint8_t prevMidiFX = activeDrumKit.drumKeys[selDrumKey].midifx;
+
+ if(mfxIndex != prevMidiFX && prevMidiFX < NUM_MIDIFX_GROUPS)
+ {
+ drumKeyUp(selDrumKey + 1);
+ }
+
+ activeDrumKit.drumKeys[selDrumKey].midifx = mfxIndex;
+
+ if(mfxQuickEdit_)
+ {
+ // Change the MidiFX Group being edited
+ if(mfxIndex < NUM_MIDIFX_GROUPS && mfxIndex != quickEditMfxIndex_)
+ {
+ enableSubmode(&subModeMidiFx[mfxIndex]);
+ subModeMidiFx[mfxIndex].enablePassthrough();
+ quickEditMfxIndex_ = mfxIndex;
+ dispMsg = false;
+ }
+ else if(mfxIndex >= NUM_MIDIFX_GROUPS)
+ {
+ disableSubmode();
+ }
+ }
+
+ for (uint8_t i = 0; i < NUM_MIDIFX_GROUPS; i++)
+ {
+ subModeMidiFx[i].setSelected(i == mfxIndex);
+ }
+
+ if (dispMsg)
+ {
+ if (mfxIndex < NUM_MIDIFX_GROUPS)
+ {
+ omxDisp.displayMessageTimed("MidiFX " + String(mfxIndex + 1), 5);
+ }
+ else
+ {
+ omxDisp.displayMessageTimed("MidiFX Off", 5);
+ }
+ }
+}
+
+void OmxModeDrum::onPotChanged(int potIndex, int prevValue, int newValue, int analogDelta)
+{
+ if (isSubmodeEnabled() && activeSubmode->usesPots())
+ {
+ activeSubmode->onPotChanged(potIndex, prevValue, newValue, analogDelta);
+ return;
+ }
+
+ auto activeMacro = getActiveMacro();
+
+ bool macroConsumesPots = false;
+ if (activeMacro != nullptr)
+ {
+ macroConsumesPots = activeMacro->consumesPots();
+ }
+
+ // Note, these get sent even if macro mode is not active
+ if (macroConsumesPots)
+ {
+ activeMacro->onPotChanged(potIndex, prevValue, newValue, analogDelta);
+ }
+ else
+ {
+ omxUtil.sendPots(potIndex, sysSettings.midiChannel);
+ }
+
+ omxDisp.setDirty();
+}
+
+void OmxModeDrum::onClockTick()
+{
+ for (uint8_t i = 0; i < 5; i++)
+ {
+ // Lets them do things in background
+ subModeMidiFx[i].onClockTick();
+ }
+}
+
+void OmxModeDrum::loopUpdate(Micros elapsedTime)
+{
+ for (uint8_t i = 0; i < 5; i++)
+ {
+ // Lets them do things in background
+ subModeMidiFx[i].loopUpdate();
+ }
+
+ // Can be modified by scale MidiFX
+ musicScale->calculateScaleIfModified(scaleConfig.scaleRoot, scaleConfig.scalePattern);
+
+}
+
+bool OmxModeDrum::isDrumKeyHeld()
+{
+ if(isSubmodeEnabled()) return false;
+
+ for(uint8_t i = 1; i < 27; i++)
+ {
+ if(midiSettings.midiKeyState[i] >= 0)
+ {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool OmxModeDrum::getEncoderSelect()
+{
+ return encoderSelect && !midiSettings.midiAUX && !isDrumKeyHeld();
+}
+
+void OmxModeDrum::onEncoderChanged(Encoder::Update enc)
+{
+ if (isSubmodeEnabled())
+ {
+ activeSubmode->onEncoderChanged(enc);
+ return;
+ }
+
+ bool macroConsumesDisplay = false;
+
+ if (macroActive_ && activeMacro_ != nullptr)
+ {
+ macroConsumesDisplay = activeMacro_->consumesDisplay();
+ }
+
+ if (macroConsumesDisplay)
+ {
+ activeMacro_->onEncoderChanged(enc);
+ return;
+ }
+
+ if (getEncoderSelect())
+ {
+ // onEncoderChangedSelectParam(enc);
+ params.changeParam(enc.dir());
+ omxDisp.setDirty();
+ return;
+ }
+
+ auto amt = enc.accel(5); // where 5 is the acceleration factor if you want it, 0 if you don't)
+
+ int8_t selPage = params.getSelPage();
+ int8_t selParam = params.getSelParam() + 1; // Add one for readability
+
+ if (selPage == DRUMPAGE_DRUMKEY)
+ {
+ auto drumKey = activeDrumKit.drumKeys[selDrumKey];
+
+ if (selParam == 1) // NoteNum
+ {
+ drumKey.noteNum = constrain(drumKey.noteNum + amt, 0, 127);
+ }
+ else if (selParam == 2) // Chan
+ {
+ drumKey.chan = constrain(drumKey.chan + amt, 1, 16);
+ }
+ else if (selParam == 3) // Vel
+ {
+ drumKey.vel = constrain(drumKey.vel + amt, 0, 127);
+ }
+ else if (selParam == 4) // MidiFX Slot
+ {
+ int midiFX = drumKey.midifx;
+ if(midiFX > NUM_MIDIFX_GROUPS)
+ {
+ midiFX = -1;
+ }
+ midiFX = constrain(midiFX + amt, -1, NUM_MIDIFX_GROUPS - 1);
+ if(midiFX < 0)
+ {
+ midiFX = 127;
+ }
+ drumKey.midifx = midiFX;
+ selectMidiFx(midiFX, false);
+ }
+
+ // Apply changes
+ activeDrumKit.drumKeys[selDrumKey] = drumKey;
+ }
+ else if (selPage == DRUMPAGE_DRUMKEY2)
+ {
+ auto drumKey = activeDrumKit.drumKeys[selDrumKey];
+
+ if (selParam == 1) // Hue
+ {
+ drumKey.hue = constrain(drumKey.hue + amt, 0, 255);
+ omxLeds.setDirty();
+ }
+
+ // Apply changes
+ activeDrumKit.drumKeys[selDrumKey] = drumKey;
+ }
+ else if (selPage == DRUMPAGE_POTSANDMACROS)
+ {
+ if (selParam == 1)
+ {
+ potSettings.potbank = constrain(potSettings.potbank + amt, 0, NUM_CC_BANKS - 1);
+ }
+ if (selParam == 2)
+ {
+ midiSettings.midiSoftThru = constrain(midiSettings.midiSoftThru + amt, 0, 1);
+ }
+ if (selParam == 3)
+ {
+ midiMacroConfig.midiMacro = constrain(midiMacroConfig.midiMacro + amt, 0, nummacromodes);
+ }
+ if (selParam == 4)
+ {
+ midiMacroConfig.midiMacroChan = constrain(midiMacroConfig.midiMacroChan + amt, 1, 16);
+ }
+ }
+ else if (selPage == DRUMPAGE_SCALES)
+ {
+ if (selParam == 1)
+ {
+ int prevRoot = scaleConfig.scaleRoot;
+ scaleConfig.scaleRoot = constrain(scaleConfig.scaleRoot + amt, 0, 12 - 1);
+ if (prevRoot != scaleConfig.scaleRoot)
+ {
+ musicScale->calculateScale(scaleConfig.scaleRoot, scaleConfig.scalePattern);
+ }
+ }
+ if (selParam == 2)
+ {
+ int prevPat = scaleConfig.scalePattern;
+ scaleConfig.scalePattern = constrain(scaleConfig.scalePattern + amt, -1, musicScale->getNumScales() - 1);
+ if (prevPat != scaleConfig.scalePattern)
+ {
+ omxDisp.displayMessage(musicScale->getScaleName(scaleConfig.scalePattern));
+ musicScale->calculateScale(scaleConfig.scaleRoot, scaleConfig.scalePattern);
+ }
+ }
+ if (selParam == 3)
+ {
+ scaleConfig.lockScale = constrain(scaleConfig.lockScale + amt, 0, 1);
+ }
+ if (selParam == 4)
+ {
+ scaleConfig.group16 = constrain(scaleConfig.group16 + amt, 0, 1);
+ }
+ }
+ else if(selPage == DRUMPAGE_CFG)
+ {
+ if (selParam == 3)
+ {
+ clockConfig.globalQuantizeStepIndex = constrain(clockConfig.globalQuantizeStepIndex + amt, 0, kNumArpRates - 1);
+ }
+ else if (selParam == 4)
+ {
+ cvNoteUtil.triggerMode = constrain(cvNoteUtil.triggerMode + amt, 0, 1);
+ }
+ }
+
+ omxDisp.setDirty();
+}
+
+void OmxModeDrum::onEncoderButtonDown()
+{
+ if (isSubmodeEnabled())
+ {
+ activeSubmode->onEncoderButtonDown();
+ return;
+ }
+
+ bool macroConsumesDisplay = false;
+ if (macroActive_ && activeMacro_ != nullptr)
+ {
+ macroConsumesDisplay = activeMacro_->consumesDisplay();
+ }
+
+ if (macroConsumesDisplay)
+ {
+ activeMacro_->onEncoderButtonDown();
+ return;
+ }
+
+ if (params.getSelPage() == DRUMPAGE_DRUMKEY2)
+ {
+ auto selParam = params.getSelParam();
+
+ if (selParam == 1)
+ {
+ randomizeHues();
+ omxDisp.isDirty();
+ omxLeds.isDirty();
+ return;
+ }
+ else if (selParam == 2) // Copy
+ {
+ tempDrumKey.CopyFrom(activeDrumKit.drumKeys[selDrumKey]);
+ omxDisp.displayMessage("Copied " + String(selDrumKey + 1));
+ omxDisp.isDirty();
+ omxLeds.isDirty();
+ return;
+ }
+ else if (selParam == 3) // Paste
+ {
+ activeDrumKit.drumKeys[selDrumKey].CopyFrom(tempDrumKey);
+ omxDisp.displayMessage("Pasted " + String(selDrumKey + 1));
+ omxDisp.isDirty();
+ omxLeds.isDirty();
+ return;
+ }
+ }
+
+ if (params.getSelPage() == DRUMPAGE_CFG && params.getSelParam() == 0)
+ {
+ enableSubmode(&subModePotConfig_);
+ omxDisp.isDirty();
+ return;
+ }
+
+ encoderSelect = !encoderSelect;
+ omxDisp.isDirty();
+}
+
+void OmxModeDrum::onEncoderButtonUp()
+{
+}
+
+void OmxModeDrum::onEncoderButtonDownLong()
+{
+}
+
+bool OmxModeDrum::shouldBlockEncEdit()
+{
+ if (isSubmodeEnabled())
+ {
+ return activeSubmode->shouldBlockEncEdit();
+ }
+
+ if (macroActive_)
+ {
+ return true;
+ }
+
+ return false;
+}
+
+void OmxModeDrum::saveKit(uint8_t saveIndex)
+{
+ drumKits[saveIndex].CopyFrom(activeDrumKit);
+ selDrumKit = saveIndex;
+}
+
+void OmxModeDrum::loadKit(uint8_t loadIndex)
+{
+ activeDrumKit.CopyFrom(drumKits[loadIndex]);
+ selDrumKit = loadIndex;
+}
+
+void OmxModeDrum::onKeyUpdate(OMXKeypadEvent e)
+{
+ if (isSubmodeEnabled())
+ {
+ if (activeSubmode->onKeyUpdate(e))
+ return;
+ }
+
+ int thisKey = e.key();
+
+ // Aux double click toggle macro
+ if (!isSubmodeEnabled() && midiMacroConfig.midiMacro > 0)
+ {
+ if (!macroActive_)
+ {
+ // Enter M8 Mode
+ if (!e.down() && thisKey == 0 && e.clicks() == 2)
+ {
+ midiSettings.midiAUX = false;
+
+ activeMacro_ = getActiveMacro();
+ if (activeMacro_ != nullptr)
+ {
+ macroActive_ = true;
+ activeMacro_->setEnabled(true);
+ activeMacro_->setScale(musicScale);
+ omxLeds.setDirty();
+ omxDisp.setDirty();
+ return;
+ }
+ return;
+ }
+ }
+ else // Macro mode active
+ {
+ if (!e.down() && thisKey == 0 && e.clicks() == 2)
+ {
+ // exit macro mode
+ if (activeMacro_ != nullptr)
+ {
+ activeMacro_->setEnabled(false);
+ activeMacro_ = nullptr;
+ }
+
+ midiSettings.midiAUX = false;
+ macroActive_ = false;
+ omxLeds.setDirty();
+ omxDisp.setDirty();
+
+ // Clear LEDs
+ // for (int m = 1; m < LED_COUNT; m++)
+ // {
+ // strip.setPixelColor(m, LEDOFF);
+ // }
+ }
+ else
+ {
+ if (activeMacro_ != nullptr)
+ {
+ activeMacro_->onKeyUpdate(e);
+ }
+ }
+ return;
+ }
+ }
+
+ if (onKeyUpdateSelMidiFX(e))
+ return;
+
+ // REGULAR KEY PRESSES
+ if (!e.held())
+ { // IGNORE LONG PRESS EVENTS
+ if (e.down() && thisKey != 0)
+ {
+ bool keyConsumed = false; // If used for aux, key will be consumed and not send notes.
+
+ if (midiSettings.midiAUX) // Aux mode
+ {
+ keyConsumed = true;
+
+ // if (thisKey == 11 || thisKey == 12) // Change Octave
+ // {
+ // int amt = thisKey == 11 ? -1 : 1;
+ // midiSettings.octave = constrain(midiSettings.octave + amt, -5, 4);
+ // }
+ if (!mfxQuickEdit_ && (thisKey == 1 || thisKey == 2)) // Change Param selection
+ {
+ if (thisKey == 1)
+ {
+ params.decrementParam();
+ }
+ else if (thisKey == 2)
+ {
+ params.incrementParam();
+ }
+ }
+ else if(thisKey == 3)
+ {
+ changeMode(DRUMMODE_LOADKIT);
+ return;
+ }
+ else if(thisKey == 4)
+ {
+ changeMode(DRUMMODE_SAVEKIT);
+ return;
+ }
+ else if (thisKey == 11 || thisKey == 12)
+ {
+ saveKit(selDrumKit);
+
+ int8_t amt = thisKey == 11 ? -1 : 1;
+ uint8_t newKitIndex = (selDrumKit + NUM_DRUM_KITS + amt) % NUM_DRUM_KITS;
+ loadKit(newKitIndex);
+
+ omxDisp.displayMessage("Loaded " + String(newKitIndex + 1));
+ }
+ }
+
+ if (!keyConsumed)
+ {
+ drumKeyDown(thisKey);
+ }
+ }
+ else if (!e.down() && thisKey != 0)
+ {
+ drumKeyUp(thisKey);
+ }
+ }
+
+ // AUX KEY
+ if (e.down() && thisKey == 0)
+ {
+ if (!macroActive_)
+ {
+ midiSettings.midiAUX = true;
+ }
+ }
+ else if (!e.down() && thisKey == 0)
+ {
+ if (midiSettings.midiAUX)
+ {
+ midiSettings.midiAUX = false;
+ }
+ }
+
+ omxLeds.setDirty();
+ omxDisp.setDirty();
+}
+
+bool OmxModeDrum::onKeyUpdateSelMidiFX(OMXKeypadEvent e)
+{
+ int thisKey = e.key();
+
+ bool keyConsumed = false;
+
+ uint8_t mfxIndex = activeDrumKit.drumKeys[selDrumKey].midifx;
+
+ if (!e.held())
+ {
+ // Double Click to edit midi fx
+ if (!e.down() && e.clicks() == 2 && thisKey >= 6 && thisKey < 11)
+ {
+ if (midiSettings.midiAUX) // Aux mode
+ {
+ enableSubmode(&subModeMidiFx[thisKey - 6]);
+ keyConsumed = true;
+ }
+ }
+
+ if (e.down() && thisKey != 0)
+ {
+ if (midiSettings.midiAUX) // Aux mode
+ {
+ if (mfxQuickEdit_ && thisKey == 1)
+ {
+ subModeMidiFx[quickEditMfxIndex_].selectPrevMFXSlot();
+ }
+ else if (mfxQuickEdit_ && thisKey == 2)
+ {
+ subModeMidiFx[quickEditMfxIndex_].selectNextMFXSlot();
+ }
+ else if (thisKey == 5)
+ {
+ keyConsumed = true;
+ // Turn off midiFx
+ selectMidiFx(127, true);
+ // mfxIndex_ = 127;
+ }
+ else if (thisKey >= 6 && thisKey < 11)
+ {
+ keyConsumed = true;
+ selectMidiFx(thisKey - 6, true);
+ // Change active midiFx
+ // mfxIndex_ = thisKey - 6;
+ }
+ else if (thisKey == 20) // MidiFX Passthrough
+ {
+ keyConsumed = true;
+ if (mfxIndex < NUM_MIDIFX_GROUPS)
+ {
+ enableSubmode(&subModeMidiFx[mfxIndex]);
+ subModeMidiFx[mfxIndex].enablePassthrough();
+ mfxQuickEdit_ = true;
+ quickEditMfxIndex_ = mfxIndex;
+ midiSettings.midiAUX = false;
+ }
+ else
+ {
+ omxDisp.displayMessage(mfxOffMsg);
+ }
+ }
+ else if (thisKey == 22) // Goto arp params
+ {
+ keyConsumed = true;
+ if (mfxIndex < NUM_MIDIFX_GROUPS)
+ {
+ enableSubmode(&subModeMidiFx[mfxIndex]);
+ subModeMidiFx[mfxIndex].gotoArpParams();
+ midiSettings.midiAUX = false;
+ }
+ else
+ {
+ omxDisp.displayMessage(mfxOffMsg);
+ }
+ }
+ else if (thisKey == 23) // Next arp pattern
+ {
+ keyConsumed = true;
+ if (mfxIndex < NUM_MIDIFX_GROUPS)
+ {
+ subModeMidiFx[mfxIndex].nextArpPattern();
+ }
+ else
+ {
+ omxDisp.displayMessage(mfxOffMsg);
+ }
+ }
+ else if (thisKey == 24) // Next arp octave
+ {
+ keyConsumed = true;
+ if (mfxIndex < NUM_MIDIFX_GROUPS)
+ {
+ subModeMidiFx[mfxIndex].nextArpOctRange();
+ }
+ else
+ {
+ omxDisp.displayMessage(mfxOffMsg);
+ }
+ }
+ else if (thisKey == 25)
+ {
+ keyConsumed = true;
+ if (mfxIndex < NUM_MIDIFX_GROUPS)
+ {
+ subModeMidiFx[mfxIndex].toggleArpHold();
+
+ if (subModeMidiFx[mfxIndex].isArpHoldOn())
+ {
+ omxDisp.displayMessageTimed("Arp Hold: On", 5);
+ }
+ else
+ {
+ omxDisp.displayMessageTimed("Arp Hold: Off", 5);
+ }
+ }
+ else
+ {
+ omxDisp.displayMessage(mfxOffMsg);
+ }
+ }
+ else if (thisKey == 26)
+ {
+ keyConsumed = true;
+ if (mfxIndex < NUM_MIDIFX_GROUPS)
+ {
+ subModeMidiFx[mfxIndex].toggleArp();
+
+ if (subModeMidiFx[mfxIndex].isArpOn())
+ {
+ omxDisp.displayMessageTimed("Arp On", 5);
+ }
+ else
+ {
+ omxDisp.displayMessageTimed("Arp Off", 5);
+ }
+ }
+ else
+ {
+ omxDisp.displayMessage(mfxOffMsg);
+ }
+ }
+ }
+ }
+ }
+
+ return keyConsumed;
+}
+
+bool OmxModeDrum::onKeyHeldSelMidiFX(OMXKeypadEvent e)
+{
+ int thisKey = e.key();
+
+ bool keyConsumed = false;
+
+ if (midiSettings.midiAUX) // Aux mode
+ {
+ // Enter MidiFX mode
+ if (thisKey >= 6 && thisKey < 11)
+ {
+ keyConsumed = true;
+ enableSubmode(&subModeMidiFx[thisKey - 6]);
+ }
+ }
+
+ return keyConsumed;
+}
+
+void OmxModeDrum::onKeyHeldUpdate(OMXKeypadEvent e)
+{
+ if (isSubmodeEnabled())
+ {
+ activeSubmode->onKeyHeldUpdate(e);
+ return;
+ }
+
+ if (onKeyHeldSelMidiFX(e))
+ return;
+}
+
+// TODO : Instantiate these outside of class so they are global
+midimacro::MidiMacroInterface *OmxModeDrum::getActiveMacro()
+{
+ switch (midiMacroConfig.midiMacro)
+ {
+ case 1:
+ return &m8Macro_;
+ case 2:
+ return &nornsMarco_;
+ case 3:
+ return &delugeMacro_;
+ }
+ return nullptr;
+}
+
+void OmxModeDrum::updateLEDs()
+{
+ if (isSubmodeEnabled())
+ {
+ if (activeSubmode->updateLEDs())
+ return;
+ }
+
+ bool blinkState = omxLeds.getBlinkState();
+ bool slowBlink = omxLeds.getSlowBlinkState();
+
+ if (midiSettings.midiAUX)
+ {
+ // Blink left/right keys for octave select indicators.
+ auto color1 = LIME;
+ auto color2 = MAGENTA;
+
+ for (int q = 1; q < LED_COUNT; q++)
+ {
+ if (midiSettings.midiKeyState[q] == -1)
+ {
+ if (colorConfig.midiBg_Hue == 0)
+ {
+ strip.setPixelColor(q, LEDOFF);
+ }
+ else if (colorConfig.midiBg_Hue == 32)
+ {
+ strip.setPixelColor(q, LOWWHITE);
+ }
+ else
+ {
+ strip.setPixelColor(q, strip.ColorHSV(colorConfig.midiBg_Hue, colorConfig.midiBg_Sat, colorConfig.midiBg_Brightness));
+ }
+ }
+ }
+ strip.setPixelColor(0, RED);
+ strip.setPixelColor(1, color1);
+ strip.setPixelColor(2, color2);
+
+ strip.setPixelColor(3, BLUE); // Load
+ strip.setPixelColor(4, ORANGE); // Save
+
+ omxLeds.drawOctaveKeys(11, 12, midiSettings.octave);
+
+ uint8_t mfxIndex = activeDrumKit.drumKeys[selDrumKey].midifx;
+
+ // MidiFX off
+ strip.setPixelColor(5, (mfxIndex >= NUM_MIDIFX_GROUPS ? colorConfig.selMidiFXGRPOffColor : colorConfig.midiFXGRPOffColor));
+
+ for (uint8_t i = 0; i < NUM_MIDIFX_GROUPS; i++)
+ {
+ auto mfxColor = (i == mfxIndex) ? colorConfig.selMidiFXGRPColor : colorConfig.midiFXGRPColor;
+
+ strip.setPixelColor(6 + i, mfxColor);
+ }
+
+ strip.setPixelColor(20, mfxQuickEdit_ && blinkState ? LEDOFF : colorConfig.mfxQuickEdit);
+ strip.setPixelColor(22, colorConfig.gotoArpParams);
+ strip.setPixelColor(23, colorConfig.nextArpPattern);
+
+ if (mfxIndex < NUM_MIDIFX_GROUPS)
+ {
+ uint8_t octaveRange = subModeMidiFx[mfxIndex].getArpOctaveRange();
+ if (octaveRange == 0)
+ {
+ strip.setPixelColor(24, colorConfig.nextArpOctave);
+ }
+ else
+ {
+ // Serial.println("Blink Octave: " + String(octaveRange));
+ bool blinkOctave = omxLeds.getBlinkPattern(octaveRange);
+
+ strip.setPixelColor(24, blinkOctave ? colorConfig.nextArpOctave : LEDOFF);
+ }
+
+ bool isOn = subModeMidiFx[mfxIndex].isArpOn() && blinkState;
+ bool isHoldOn = subModeMidiFx[mfxIndex].isArpHoldOn();
+
+ strip.setPixelColor(25, isHoldOn ? colorConfig.arpHoldOn : colorConfig.arpHoldOff);
+ strip.setPixelColor(26, isOn ? colorConfig.arpOn : colorConfig.arpOff);
+ }
+ else
+ {
+ strip.setPixelColor(25, colorConfig.arpHoldOff);
+ strip.setPixelColor(26, colorConfig.arpOff);
+ }
+ }
+ else
+ {
+ for(uint8_t k = 1; k < 27; k++)
+ {
+ auto drumKey = activeDrumKit.drumKeys[k-1];
+
+ bool drumKeyOn = midiSettings.midiKeyState[k] >= 0;
+
+ if(k-1 == selDrumKey)
+ {
+ drumKeyOn = drumKeyOn || slowBlink;
+ }
+
+ uint16_t hue = map(drumKey.hue, 0, 255, 0, 65535);
+
+ strip.setPixelColor(k, strip.gamma32(strip.ColorHSV(hue, drumKeyOn ? 200 : 255, drumKeyOn ? 255 : 160)));
+ }
+ }
+
+ // if (isSubmodeEnabled())
+ // {
+ // bool blinkStateSlow = omxLeds.getSlowBlinkState();
+
+ // auto auxColor = (blinkStateSlow ? RED : LEDOFF);
+ // strip.setPixelColor(0, auxColor);
+ // }
+}
+
+void OmxModeDrum::onDisplayUpdate()
+{
+ if (isSubmodeEnabled())
+ {
+ if (omxLeds.isDirty())
+ {
+ updateLEDs();
+ }
+ activeSubmode->onDisplayUpdate();
+ return;
+ }
+
+ bool macroConsumesDisplay = false;
+
+ if (macroActive_ && activeMacro_ != nullptr)
+ {
+ activeMacro_->drawLEDs();
+ macroConsumesDisplay = activeMacro_->consumesDisplay();
+ }
+ else
+ {
+ if (omxLeds.isDirty())
+ {
+ updateLEDs();
+ }
+ }
+
+ if (macroConsumesDisplay)
+ {
+ activeMacro_->onDisplayUpdate();
+ }
+ else
+ {
+ if (omxDisp.isDirty())
+ { // DISPLAY
+ if (!encoderConfig.enc_edit)
+ {
+ if (params.getSelPage() == DRUMPAGE_DRUMKEY)
+ {
+ auto drumKey = activeDrumKit.drumKeys[selDrumKey];
+
+ omxDisp.clearLegends();
+
+ omxDisp.legends[0] = "NOTE";
+ omxDisp.legends[1] = "CH";
+ omxDisp.legends[2] = "VEL";
+ omxDisp.legends[3] = "FX#";
+ omxDisp.legendVals[0] = drumKey.noteNum;
+ omxDisp.legendVals[1] = drumKey.chan;
+ omxDisp.legendVals[2] = drumKey.vel;
+ if(drumKey.midifx >= NUM_MIDIFX_GROUPS)
+ {
+ omxDisp.legendText[3] = "OFF";
+ }
+ else
+ {
+ omxDisp.legendVals[3] = drumKey.midifx + 1;
+ }
+ }
+ else if (params.getSelPage() == DRUMPAGE_DRUMKEY2)
+ {
+ auto drumKey = activeDrumKit.drumKeys[selDrumKey];
+
+ omxDisp.clearLegends();
+
+ omxDisp.legends[0] = "HUE";
+ omxDisp.legends[1] = "HUE";
+ omxDisp.legends[2] = "COPY";
+ omxDisp.legends[3] = "PAST";
+ omxDisp.legendVals[0] = drumKey.hue;
+ omxDisp.legendText[1] = "RND";
+ omxDisp.legendVals[2] = selDrumKey + 1;
+ omxDisp.legendVals[3] = selDrumKey + 1;
+ }
+ else if (params.getSelPage() == DRUMPAGE_INSPECT)
+ {
+ omxDisp.clearLegends();
+
+ omxDisp.legends[0] = "P CC";
+ omxDisp.legends[1] = "P VAL";
+ omxDisp.legends[2] = "NOTE";
+ omxDisp.legends[3] = "VEL";
+ omxDisp.legendVals[0] = potSettings.potCC;
+ omxDisp.legendVals[1] = potSettings.potVal;
+ omxDisp.legendVals[2] = midiSettings.midiLastNote;
+ omxDisp.legendVals[3] = midiSettings.midiLastVel;
+ }
+ else if (params.getSelPage() == DRUMPAGE_POTSANDMACROS) // SUBMODE_MIDI3
+ {
+ omxDisp.clearLegends();
+
+ omxDisp.legends[0] = "PBNK"; // Potentiometer Banks
+ omxDisp.legends[1] = "THRU"; // MIDI thru (usb to hardware)
+ omxDisp.legends[2] = "MCRO"; // Macro mode
+ omxDisp.legends[3] = "M-CH";
+ omxDisp.legendVals[0] = potSettings.potbank + 1;
+ omxDisp.legendText[1] = midiSettings.midiSoftThru ? "On" : "Off";
+ omxDisp.legendText[2] = macromodes[midiMacroConfig.midiMacro];
+ omxDisp.legendVals[3] = midiMacroConfig.midiMacroChan;
+ }
+ else if (params.getSelPage() == DRUMPAGE_SCALES) // SCALES
+ {
+ omxDisp.clearLegends();
+ omxDisp.legends[0] = "ROOT";
+ omxDisp.legends[1] = "SCALE";
+ omxDisp.legends[2] = "LOCK";
+ omxDisp.legends[3] = "GROUP";
+ omxDisp.legendVals[0] = -127;
+ if (scaleConfig.scalePattern < 0)
+ {
+ omxDisp.legendVals[1] = -127;
+ omxDisp.legendText[1] = "Off";
+ }
+ else
+ {
+ omxDisp.legendVals[1] = scaleConfig.scalePattern;
+ }
+
+ omxDisp.legendVals[2] = -127;
+ omxDisp.legendVals[3] = -127;
+
+ omxDisp.legendText[0] = musicScale->getNoteName(scaleConfig.scaleRoot);
+ omxDisp.legendText[2] = scaleConfig.lockScale ? "On" : "Off";
+ omxDisp.legendText[3] = scaleConfig.group16 ? "On" : "Off";
+ }
+ else if (params.getSelPage() == DRUMPAGE_CFG) // CONFIG
+ {
+ omxDisp.clearLegends();
+ omxDisp.setLegend(0,"P CC", "CFG");
+ omxDisp.setLegend(1,"CLR", "STOR");
+ omxDisp.setLegend(2,"QUANT", "1/" + String(kArpRates[clockConfig.globalQuantizeStepIndex]));
+ omxDisp.setLegend(3,"CV M", cvNoteUtil.getTriggerModeDispName());
+ }
+
+ omxDisp.dispGenericMode2(params.getNumPages(), params.getSelPage(), params.getSelParam(), getEncoderSelect());
+ }
+ }
+ }
+}
+
+// void onDisplayUpdateLoadKit()
+// {
+
+// }
+
+// incoming midi note on
+void OmxModeDrum::inMidiNoteOn(byte channel, byte note, byte velocity)
+{
+ // midiSettings.midiLastNote = note;
+ // midiSettings.midiLastVel = velocity;
+ // int whatoct = (note / 12);
+ // int thisKey;
+ // uint32_t keyColor = MIDINOTEON;
+
+ // if ((whatoct % 2) == 0)
+ // {
+ // thisKey = note - (12 * whatoct);
+ // }
+ // else
+ // {
+ // thisKey = note - (12 * whatoct) + 12;
+ // }
+ // if (whatoct == 0)
+ // { // ORANGE,YELLOW,GREEN,MAGENTA,CYAN,BLUE,LIME,LTPURPLE
+ // }
+ // else if (whatoct == 1)
+ // {
+ // keyColor = ORANGE;
+ // }
+ // else if (whatoct == 2)
+ // {
+ // keyColor = YELLOW;
+ // }
+ // else if (whatoct == 3)
+ // {
+ // keyColor = GREEN;
+ // }
+ // else if (whatoct == 4)
+ // {
+ // keyColor = MAGENTA;
+ // }
+ // else if (whatoct == 5)
+ // {
+ // keyColor = CYAN;
+ // }
+ // else if (whatoct == 6)
+ // {
+ // keyColor = LIME;
+ // }
+ // else if (whatoct == 7)
+ // {
+ // keyColor = CYAN;
+ // }
+ // strip.setPixelColor(midiKeyMap[thisKey], keyColor); // Set pixel's color (in RAM)
+ // // dirtyPixels = true;
+ // strip.show();
+ // omxDisp.setDirty();
+}
+
+void OmxModeDrum::inMidiNoteOff(byte channel, byte note, byte velocity)
+{
+ // int whatoct = (note / 12);
+ // int thisKey;
+ // if ((whatoct % 2) == 0)
+ // {
+ // thisKey = note - (12 * whatoct);
+ // }
+ // else
+ // {
+ // thisKey = note - (12 * whatoct) + 12;
+ // }
+ // strip.setPixelColor(midiKeyMap[thisKey], LEDOFF); // Set pixel's color (in RAM)
+ // // dirtyPixels = true;
+ // strip.show();
+ // omxDisp.setDirty();
+}
+
+void OmxModeDrum::inMidiControlChange(byte channel, byte control, byte value)
+{
+ auto activeMacro = getActiveMacro();
+
+ if (activeMacro != nullptr)
+ {
+ activeMacro->inMidiControlChange(channel, control, value);
+ }
+}
+
+void OmxModeDrum::SetScale(MusicScales *scale)
+{
+ this->musicScale = scale;
+ m8Macro_.setScale(scale);
+ nornsMarco_.setScale(scale);
+}
+
+void OmxModeDrum::randomizeHues()
+{
+ for(uint8_t i = 0; i < 26; i++)
+ {
+ activeDrumKit.drumKeys[i].hue = random(255);
+ }
+}
+
+void OmxModeDrum::enableSubmode(SubmodeInterface *subMode)
+{
+ if (activeSubmode != nullptr)
+ {
+ activeSubmode->setEnabled(false);
+ }
+
+ activeSubmode = subMode;
+ activeSubmode->setEnabled(true);
+ omxDisp.setDirty();
+}
+
+void OmxModeDrum::disableSubmode()
+{
+ if (activeSubmode != nullptr)
+ {
+ activeSubmode->setEnabled(false);
+ }
+
+ midiSettings.midiAUX = false;
+ mfxQuickEdit_ = false;
+ activeSubmode = nullptr;
+ omxDisp.setDirty();
+}
+
+bool OmxModeDrum::isSubmodeEnabled()
+{
+ if (activeSubmode == nullptr)
+ return false;
+
+ if (activeSubmode->isEnabled() == false)
+ {
+ disableSubmode();
+ midiSettings.midiAUX = false;
+ return false;
+ }
+
+ return true;
+}
+
+void OmxModeDrum::drumKeyDown(uint8_t keyIndex)
+{
+ auto drumKey = activeDrumKit.drumKeys[keyIndex - 1];
+
+ MidiNoteGroup noteGroup = omxUtil.midiDrumNoteOn(keyIndex, drumKey.noteNum, drumKey.vel, drumKey.chan);
+
+ if (noteGroup.noteNumber == 255)
+ return;
+
+ selDrumKey = keyIndex - 1;
+
+ noteGroup.unknownLength = true;
+ noteGroup.prevNoteNumber = noteGroup.noteNumber;
+
+ if (drumKey.midifx < NUM_MIDIFX_GROUPS)
+ {
+ subModeMidiFx[drumKey.midifx].noteInput(noteGroup);
+ }
+ else
+ {
+ onNotePostFX(noteGroup);
+ }
+}
+
+void OmxModeDrum::drumKeyUp(uint8_t keyIndex)
+{
+ MidiNoteGroup noteGroup = omxUtil.midiDrumNoteOff(keyIndex);
+
+ if (noteGroup.noteNumber == 255)
+ return;
+
+ auto drumKey = activeDrumKit.drumKeys[keyIndex - 1];
+
+ noteGroup.unknownLength = true;
+ noteGroup.prevNoteNumber = noteGroup.noteNumber;
+
+ if (drumKey.midifx < NUM_MIDIFX_GROUPS)
+ {
+ subModeMidiFx[drumKey.midifx].noteInput(noteGroup);
+ }
+ else
+ {
+ onNotePostFX(noteGroup);
+ }
+}
+
+// Called via doNoteOnForwarder
+void OmxModeDrum::doNoteOn(uint8_t keyIndex)
+{
+ MidiNoteGroup noteGroup = omxUtil.midiNoteOn2(musicScale, keyIndex, midiSettings.defaultVelocity, sysSettings.midiChannel);
+
+ if (noteGroup.noteNumber == 255)
+ return;
+
+ // Serial.println("doNoteOn: " + String(noteGroup.noteNumber));
+
+ noteGroup.unknownLength = true;
+ noteGroup.prevNoteNumber = noteGroup.noteNumber;
+
+ onNotePostFX(noteGroup);
+
+ // if (mfxIndex_ < NUM_MIDIFX_GROUPS)
+ // {
+ // subModeMidiFx[mfxIndex_].noteInput(noteGroup);
+ // // subModeMidiFx.noteInput(noteGroup);
+ // }
+ // else
+ // {
+ // onNotePostFX(noteGroup);
+ // }
+}
+
+// Called via doNoteOnForwarder
+void OmxModeDrum::doNoteOff(uint8_t keyIndex)
+{
+ MidiNoteGroup noteGroup = omxUtil.midiNoteOff2(keyIndex, sysSettings.midiChannel);
+
+ if (noteGroup.noteNumber == 255)
+ return;
+
+ // Serial.println("doNoteOff: " + String(noteGroup.noteNumber));
+
+ noteGroup.unknownLength = true;
+ noteGroup.prevNoteNumber = noteGroup.noteNumber;
+
+ onNotePostFX(noteGroup);
+
+ // if (mfxIndex_ < NUM_MIDIFX_GROUPS)
+ // {
+ // subModeMidiFx[mfxIndex_].noteInput(noteGroup);
+ // // subModeMidiFx.noteInput(noteGroup);
+ // }
+ // else
+ // {
+ // onNotePostFX(noteGroup);
+ // }
+}
+
+// Called by the midiFX group when a note exits it's FX Pedalboard
+void OmxModeDrum::onNotePostFX(MidiNoteGroup note)
+{
+ if (note.noteOff)
+ {
+ // Serial.println("OmxModeDrum::onNotePostFX noteOff: " + String(note.noteNumber));
+
+ if (note.sendMidi)
+ {
+ MM::sendNoteOff(note.noteNumber, note.velocity, note.channel);
+ }
+ if (note.sendCV)
+ {
+ cvNoteUtil.cvNoteOff(note.noteNumber);
+ }
+ }
+ else
+ {
+ if (note.unknownLength == false)
+ {
+ uint32_t noteOnMicros = note.noteonMicros; // TODO Might need to be set to current micros
+ pendingNoteOns.insert(note.noteNumber, note.velocity, note.channel, noteOnMicros, note.sendCV);
+
+ // Serial.println("StepLength: " + String(note.stepLength));
+
+ uint32_t noteOffMicros = noteOnMicros + (note.stepLength * clockConfig.step_micros);
+ pendingNoteOffs.insert(note.noteNumber, note.channel, noteOffMicros, note.sendCV);
+
+ // Serial.println("noteOnMicros: " + String(noteOnMicros));
+ // Serial.println("noteOffMicros: " + String(noteOffMicros));
+ }
+ else
+ {
+ // Serial.println("OmxModeDrum::onNotePostFX noteOn: " + String(note.noteNumber));
+
+ if (note.sendMidi)
+ {
+ midiSettings.midiLastNote = note.noteNumber;
+ midiSettings.midiLastVel = note.velocity;
+ MM::sendNoteOn(note.noteNumber, note.velocity, note.channel);
+ }
+ if (note.sendCV)
+ {
+ cvNoteUtil.cvNoteOn(note.noteNumber);
+ }
+ }
+ }
+
+ // uint32_t noteOnMicros = note.noteonMicros; // TODO Might need to be set to current micros
+ // pendingNoteOns.insert(note.noteNumber, note.velocity, note.channel, noteOnMicros, note.sendCV);
+
+ // uint32_t noteOffMicros = noteOnMicros + (note.stepLength * clockConfig.step_micros);
+ // pendingNoteOffs.insert(note.noteNumber, note.channel, noteOffMicros, note.sendCV);
+}
+
+void OmxModeDrum::onPendingNoteOff(int note, int channel)
+{
+ // Serial.println("OmxModeEuclidean::onPendingNoteOff " + String(note) + " " + String(channel));
+ // subModeMidiFx.onPendingNoteOff(note, channel);
+
+ for (uint8_t i = 0; i < NUM_MIDIFX_GROUPS; i++)
+ {
+ subModeMidiFx[i].onPendingNoteOff(note, channel);
+ }
+}
+
+int OmxModeDrum::saveToDisk(int startingAddress, Storage *storage)
+{
+ int saveSize = sizeof(DrumKit);
+
+ for (uint8_t saveIndex = 0; saveIndex < NUM_DRUM_KITS; saveIndex++)
+ {
+ auto saveBytesPtr = (byte *)(&drumKits[saveIndex]);
+ for (int j = 0; j < saveSize; j++)
+ {
+ storage->write(startingAddress + j, *saveBytesPtr++);
+ }
+
+ startingAddress += saveSize;
+ }
+
+ return startingAddress;
+}
+
+int OmxModeDrum::loadFromDisk(int startingAddress, Storage *storage)
+{
+ int saveSize = sizeof(DrumKit); // 5 * 26 = 130
+
+ // int drumKeySize = sizeof(DrumKeySettings);
+
+ // Serial.println((String)"DrumKit Size: " + saveSize + " drumKeySize: " + drumKeySize); // 5 - 130 - 1040 bytes
+
+ for (uint8_t saveIndex = 0; saveIndex < NUM_DRUM_KITS; saveIndex++)
+ {
+ // auto drumKit = DrumKit{};
+ // auto current = (byte *)&drumKit;
+ // for (int j = 0; j < saveSize; j++)
+ // {
+ // *current = storage->read(startingAddress + j);
+ // current++;
+ // }
+
+ // drumKits[saveIndex].CopyFrom(drumKit);
+
+ // Write bytes to heap
+ auto current = (byte *)&drumKits[saveIndex];
+ for (int j = 0; j < saveSize; j++)
+ {
+ *current = storage->read(startingAddress + j);
+ current++;
+ }
+
+ startingAddress += saveSize;
+ }
+
+ loadKit(0);
+
+ return startingAddress;
+}
diff --git a/Archive/OMX-27-firmware/src/modes/omx_mode_drum.h b/Archive/OMX-27-firmware/src/modes/omx_mode_drum.h
new file mode 100644
index 00000000..a3cb1c6c
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/modes/omx_mode_drum.h
@@ -0,0 +1,187 @@
+#pragma once
+
+#include "omx_mode_interface.h"
+#include "../utils/music_scales.h"
+#include "../utils/param_manager.h"
+#include "submodes/submode_midifxgroup.h"
+#include "submodes/submode_potconfig.h"
+#include "submodes/submode_preset.h"
+#include "../midifx/midifx_interface.h"
+#include "../midifx/midifx_interface.h"
+#include "../midimacro/midimacro_m8.h"
+#include "../midimacro/midimacro_norns.h"
+#include "../midimacro/midimacro_deluge.h"
+
+struct DrumKeySettings
+{
+public:
+ uint8_t noteNum = 64;
+ uint8_t chan = 1;
+ uint8_t vel = 100;
+ uint8_t midifx = 0;
+ uint8_t hue = 64;
+
+ void CopyFrom(DrumKeySettings other)
+ {
+ this->noteNum = other.noteNum;
+ this->chan = other.chan;
+ this->vel = other.vel;
+ this->midifx = other.midifx;
+ this->hue = other.hue;
+ }
+};
+
+struct DrumKit
+{
+ public:
+ DrumKeySettings drumKeys[26]; // Sorry, Aux can't be used for a drum key
+
+ void CopyFrom(DrumKit other)
+ {
+ for(uint8_t i = 0; i < 26; i++)
+ {
+ drumKeys[i].CopyFrom(other.drumKeys[i]);
+ }
+ }
+};
+
+// This mode is designed to be used with samplers or drum machines
+// Each key can be configured to whatever Note, Vel, Midi Chan you want.
+// This class is very similar to the midi keyboard, maybe we merge or inherit.
+class OmxModeDrum : public OmxModeInterface
+{
+public:
+ OmxModeDrum();
+ ~OmxModeDrum() {}
+
+ void InitSetup() override;
+ void onModeActivated() override;
+ void onModeDeactivated() override;
+
+ void onPotChanged(int potIndex, int prevValue, int newValue, int analogDelta) override;
+ void loopUpdate(Micros elapsedTime) override;
+ void onClockTick() override;
+
+ void updateLEDs() override;
+
+ void onEncoderChanged(Encoder::Update enc) override;
+ void onEncoderButtonDown() override;
+ void onEncoderButtonUp() override;
+
+ void onEncoderButtonDownLong() override;
+
+ bool shouldBlockEncEdit() override;
+
+ void onKeyUpdate(OMXKeypadEvent e) override;
+ void onKeyHeldUpdate(OMXKeypadEvent e) override;
+
+ void onDisplayUpdate() override;
+ void inMidiNoteOn(byte channel, byte note, byte velocity) override;
+ void inMidiNoteOff(byte channel, byte note, byte velocity) override;
+ void inMidiControlChange(byte channel, byte control, byte value) override;
+
+ void SetScale(MusicScales *scale);
+
+ int saveToDisk(int startingAddress, Storage *storage);
+ int loadFromDisk(int startingAddress, Storage *storage);
+private:
+ static const uint8_t NUM_DRUM_KITS = 8;
+ SubModePreset presetManager;
+ uint8_t selDrumKit;
+ uint8_t selDrumKey;
+
+ DrumKeySettings tempDrumKey; // for copy/paste
+
+ DrumKit activeDrumKit;
+ DrumKit drumKits[NUM_DRUM_KITS];
+ MusicScales *musicScale;
+
+ void changeMode(uint8_t newModeIndex);
+ void drumKeyDown(uint8_t keyIndex);
+ void drumKeyUp(uint8_t keyIndex);
+ void randomizeHues();
+
+ bool initSetup = false;
+
+ // If true, encoder selects param rather than modifies value
+ bool encoderSelect = false;
+ // void onEncoderChangedSelectParam(Encoder::Update enc);
+ ParamManager params;
+
+ bool macroActive_ = false;
+ bool mfxQuickEdit_ = false;
+ uint8_t quickEditMfxIndex_ = 0;
+
+ bool isDrumKeyHeld();
+ bool getEncoderSelect();
+
+ void onKeyUpdateLoadKit(OMXKeypadEvent e);
+ bool onKeyUpdateSelMidiFX(OMXKeypadEvent e);
+ bool onKeyHeldSelMidiFX(OMXKeypadEvent e);
+
+ // SubModes
+ SubmodeInterface *activeSubmode = nullptr;
+ SubModePotConfig subModePotConfig_;
+
+ void enableSubmode(SubmodeInterface *subMode);
+ void disableSubmode();
+ bool isSubmodeEnabled();
+
+ void doNoteOn(uint8_t keyIndex);
+ void doNoteOff(uint8_t keyIndex);
+
+ // Static glue to link a pointer to a member function
+ static void onNotePostFXForwarder(void *context, MidiNoteGroup note)
+ {
+ static_cast(context)->onNotePostFX(note);
+ }
+
+ void onNotePostFX(MidiNoteGroup note);
+
+ // Static glue to link a pointer to a member function
+ static void onPendingNoteOffForwarder(void *context, int note, int channel)
+ {
+ static_cast(context)->onPendingNoteOff(note, channel);
+ }
+
+ void onPendingNoteOff(int note, int channel);
+
+ void stopSequencers();
+
+ void selectMidiFx(uint8_t mfxIndex, bool dispMsg);
+
+ // uint8_t mfxIndex_ = 0;
+
+ midimacro::MidiMacroNorns nornsMarco_;
+ midimacro::MidiMacroM8 m8Macro_;
+ midimacro::MidiMacroDeluge delugeMacro_;
+
+ midimacro::MidiMacroInterface *activeMacro_;
+
+ midimacro::MidiMacroInterface *getActiveMacro();
+
+ void saveKit(uint8_t saveIndex);
+ void loadKit(uint8_t loadIndex);
+
+ static void doSaveKitForwarder(void *context, uint8_t kitIndex)
+ {
+ static_cast(context)->saveKit(kitIndex);
+ }
+
+ static void doLoadKitForwarder(void *context, uint8_t kitIndex)
+ {
+ static_cast(context)->loadKit(kitIndex);
+ }
+
+ // Static glue to link a pointer to a member function
+ static void doNoteOnForwarder(void *context, uint8_t keyIndex)
+ {
+ static_cast(context)->doNoteOn(keyIndex);
+ }
+
+ // Static glue to link a pointer to a member function
+ static void doNoteOffForwarder(void *context, uint8_t keyIndex)
+ {
+ static_cast(context)->doNoteOff(keyIndex);
+ }
+};
diff --git a/Archive/OMX-27-firmware/src/modes/omx_mode_euclidean.cpp b/Archive/OMX-27-firmware/src/modes/omx_mode_euclidean.cpp
new file mode 100644
index 00000000..f9a42914
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/modes/omx_mode_euclidean.cpp
@@ -0,0 +1,1554 @@
+#include "../modes/omx_mode_euclidean.h"
+#include "../config.h"
+#include "../utils/omx_util.h"
+#include "../hardware/omx_disp.h"
+#include "../hardware/omx_leds.h"
+// #include "../sequencer.h"
+#include "../modes/euclidean_sequencer.h"
+// #include "../ClearUI/ClearUI.h"
+#include "../midi/noteoffs.h"
+#include "../midi/midi.h"
+#include "../utils/logic_util.h"
+using namespace euclidean;
+
+using namespace euclidean;
+
+// enum EucModePage {
+// EUCLID_DENSITY,
+// EUCLID_XY,
+// EUCLID_NOTES,
+// EUCLID_CONFIG
+// };
+
+enum ParamModes
+{
+ PARAMMODE_MIX = 0,
+ PARAMMODE_EDIT = 1,
+ PARAMMODE_PATTERN = 2
+};
+
+enum SelEucModePage
+{
+ SELEUCLID_PAT,
+ SELEUCLID_1,
+ SELEUCLID_NOTES,
+ SELEUCLID_CFG1 // PolyRythm, Rate, Global Rate, BPM
+};
+
+const int kSelMixColor = WHITE;
+const int kMixColor = ORANGE;
+const int kMixTrigger = 0xFCD0A4;
+const int kMixMuteColor = 0x080808; // 0x1f1001;
+
+const int kSelSaveColor = WHITE;
+const int kSaveColor = DKGREEN;
+
+const int kSelEuclidColor = LBLUE;
+const int kSelEuclidTriggerColor = AMBER;
+const int kSelEuclidMuteColor = DKBLUE;
+const int kEuclidColor = DKRED;
+const int kEuclidTrigger = AMBER;
+const int kEuclidMuteColor = 0x080808; // 0x240000;
+
+const int kSelMidiFXColor = LTCYAN;
+const int kMidiFXColor = BLUE;
+
+OmxModeEuclidean::OmxModeEuclidean()
+{
+ midiKeyboard.setMidiMode();
+
+ // Setup function pointers for note ons.
+ for (uint8_t i = 0; i < kNumEuclids; i++)
+ {
+ euclids[i].setNoteOutputFunc(&OmxModeEuclidean::onNoteTriggeredForwarder, this, i);
+ }
+
+ polyRhythmMode = false;
+
+ for (uint8_t i = 0; i < kNumEuclids; i++)
+ {
+ euclids[i].setPolyRhythmMode(polyRhythmMode);
+ euclids[i].setClockDivMult(3);
+ euclids[i].setPolyRClockDivMult(3);
+
+ initEuclid_.polyRhythmMode_ = polyRhythmMode;
+ initEuclid_.polyRClockDivMultP_ = 3;
+ }
+
+ paramMode_ = PARAMMODE_EDIT;
+
+ params_[PARAMMODE_MIX].addPage(1);
+
+ params_[PARAMMODE_EDIT].addPage(1);
+ params_[PARAMMODE_EDIT].addPage(4);
+ params_[PARAMMODE_EDIT].addPage(4);
+ params_[PARAMMODE_EDIT].addPage(4);
+
+ params_[PARAMMODE_PATTERN].addPage(1);
+
+ euclids[0].setNoteNumber(36);
+ euclids[1].setNoteNumber(38);
+ euclids[2].setNoteNumber(42);
+ euclids[3].setNoteNumber(46);
+
+ euclids[4].setNoteNumber(60);
+ euclids[5].setNoteNumber(64);
+ euclids[6].setNoteNumber(67);
+ euclids[7].setNoteNumber(71);
+
+ for (uint8_t i = 0; i < kNumSaves; i++)
+ {
+ saveActivePattern(i, false);
+ }
+
+ selectedSave_ = 0;
+}
+
+void OmxModeEuclidean::InitSetup()
+{
+ initSetup = true;
+}
+
+void OmxModeEuclidean::onModeActivated()
+{
+ if (!initSetup)
+ {
+ InitSetup();
+ }
+
+ isPlaying_ = false;
+
+ // sequencer.playing = false;
+ // stopSequencers();
+ aux_ = false;
+ f1_ = false;
+ f2_ = false;
+ f3_ = false;
+ fNone_ = true;
+ // grids_.stop();
+ // grids_.loadSnapShot(grids_.playingPattern);
+ // gridsAUX = false;
+
+ omxLeds.setDirty();
+ omxDisp.setDirty();
+
+ paramMode_ = PARAMMODE_EDIT;
+ encoderSelect_ = true;
+
+ for (uint8_t i = 0; i < NUM_MIDIFX_GROUPS; i++)
+ {
+ subModeMidiFx[i].setNoteOutputFunc(&OmxModeEuclidean::onNotePostFXForwarder, this);
+ subModeMidiFx[i].setSelected(true);
+ subModeMidiFx[i].onModeChanged();
+ }
+
+ pendingNoteOffs.setNoteOffFunction(&OmxModeEuclidean::onPendingNoteOffForwarder, this);
+}
+
+void OmxModeEuclidean::onModeDeactivated()
+{
+ isPlaying_ = false;
+ // sequencer.playing = false;
+ stopSequencers();
+
+ for (uint8_t i = 0; i < NUM_MIDIFX_GROUPS; i++)
+ {
+ subModeMidiFx[i].setEnabled(false);
+ subModeMidiFx[i].setSelected(false);
+ subModeMidiFx[i].onModeChanged();
+ }
+}
+
+void OmxModeEuclidean::startSequencers()
+{
+ // pendingStart_ = true;
+ isPlaying_ = true;
+ omxUtil.startClocks();
+
+ for (u_int8_t i = 0; i < kNumEuclids; i++)
+ {
+ euclids[i].start();
+ }
+
+ for (uint8_t i = 0; i < NUM_MIDIFX_GROUPS; i++)
+ {
+ subModeMidiFx[i].setSelected(true);
+ }
+ // MM::startClock();
+
+ pendingStart_ = false;
+}
+void OmxModeEuclidean::stopSequencers()
+{
+ isPlaying_ = false;
+ pendingStart_ = false;
+
+ for (u_int8_t i = 0; i < kNumEuclids; i++)
+ {
+ euclids[i].stop();
+ }
+ omxUtil.stopClocks();
+ // MM::stopClock();
+ pendingNoteOffs.allOff();
+
+ for (uint8_t i = 0; i < NUM_MIDIFX_GROUPS; i++)
+ {
+ subModeMidiFx[i].resync();
+ }
+}
+
+void OmxModeEuclidean::onClockTick()
+{
+ // if (pendingStart_)
+ // {
+ // for (u_int8_t i = 0; i < kNumEuclids; i++)
+ // {
+ // euclids[i].start();
+ // }
+
+ // for (uint8_t i = 0; i < NUM_MIDIFX_GROUPS; i++)
+ // {
+ // subModeMidiFx[i].setSelected(true);
+ // }
+ // MM::startClock();
+
+ // pendingStart_ = false;
+ // }
+
+ for (uint8_t i = 0; i < NUM_MIDIFX_GROUPS; i++)
+ {
+ // Lets them do things in background
+ subModeMidiFx[i].onClockTick();
+ }
+
+ // euclids[0].clockTick();
+
+ // for (u_int8_t i = 0; i < kNumEuclids; i++)
+ // {
+ // euclids[i].clockTick();
+ // }
+}
+
+// void OmxModeEuclidean::drawEuclidPattern(bool *pattern, uint8_t steps)
+// {
+// if(steps == 0 || steps == 1) return;
+
+// int16_t steponHeight = 3;
+// int16_t stepoffHeight = 1;
+// int16_t stepWidth = 2;
+// int16_t halfh = gridh / 2;
+// int16_t halfw = gridw / 2;
+
+// int16_t stepint = gridw / steps - 1;
+
+// display.drawLine(0, halfh, gridw, halfh, HALFWHITE);
+
+// for (int i = 0; i < steps; i++)
+// {
+// int16_t xPos = stepint * i;
+// int16_t yPos = gridw;
+
+// if (pattern[i])
+// {
+// display.fillRect(xPos, yPos, stepWidth, steponHeight, WHITE);
+
+// }
+// else
+// {
+// display.fillRect(xPos, yPos, stepWidth, stepoffHeight, WHITE);
+// }
+// }
+
+// omxDisp.setDirty();
+// }
+
+// void OmxModeEuclidean::printEuclidPattern(bool *pattern, uint8_t steps)
+// {
+// String sOut = "";
+// for (uint8_t i = 0; i < steps; i++)
+// {
+// sOut += (pattern[i] ? "X" : "-");
+// }
+// Serial.println(sOut.c_str());
+// }
+
+ParamManager *OmxModeEuclidean::getSelectedParamMode()
+{
+ return ¶ms_[paramMode_];
+}
+
+void OmxModeEuclidean::setParamMode(uint8_t newParamMode)
+{
+ switch (newParamMode)
+ {
+ case PARAMMODE_MIX:
+ {
+ paramMode_ = PARAMMODE_MIX;
+ omxDisp.displayMessageTimed("Mix", 5);
+ setPageAndParam(0, 0, false);
+ }
+ break;
+ case PARAMMODE_EDIT:
+ {
+ paramMode_ = PARAMMODE_EDIT;
+ omxDisp.displayMessageTimed("Edit", 5);
+ setPageAndParam(0, 0, false);
+ }
+ break;
+ case PARAMMODE_PATTERN:
+ {
+ paramMode_ = PARAMMODE_PATTERN;
+ omxDisp.displayMessageTimed("Pattern", 5);
+ setPageAndParam(0, 0, false);
+ }
+ break;
+ default:
+ break;
+ }
+}
+
+void OmxModeEuclidean::setPageAndParam(uint8_t pageIndex, uint8_t paramPosition, bool editParam)
+{
+ encoderSelect_ = !editParam;
+ params_[paramMode_].setSelPage(pageIndex);
+ // selEucParams.setSelPage(pageIndex);
+ setParam(paramPosition);
+ omxDisp.setDirty();
+}
+
+void OmxModeEuclidean::setParam(uint8_t paramIndex)
+{
+ params_[paramMode_].setSelParam(paramIndex);
+
+ // selEucParams.setSelParam(paramIndex);
+
+ // // Select instrument on this page
+ // if (instLockView_ && params.getSelPage() == GRIDS_DENSITY)
+ // {
+ // lockedInst_ = paramIndex;
+ // }
+ omxDisp.setDirty();
+}
+
+void OmxModeEuclidean::onPotChanged(int potIndex, int prevValue, int newValue, int analogDelta)
+{
+ if (isSubmodeEnabled() && activeSubmode->usesPots())
+ {
+ activeSubmode->onPotChanged(potIndex, prevValue, newValue, analogDelta);
+ return;
+ }
+
+ if (midiModeception)
+ {
+ midiKeyboard.onPotChanged(potIndex, prevValue, newValue, analogDelta);
+ return;
+ }
+
+ EuclideanSequencer *activeEuclid = &euclids[selectedEuclid_];
+
+ // Serial.println(String("PotChanged ") + String(potIndex));
+
+ // --- EDIT MODE ---
+ if (paramMode_ == PARAMMODE_EDIT)
+ {
+ // Serial.println("Edit Mode");
+
+ if (analogDelta < 3)
+ return;
+
+ if (potIndex == 0)
+ {
+ // Serial.println("Rotation");
+
+ activeEuclid->setRotation(map(newValue, 0, 127, 0, 32));
+ }
+ if (potIndex == 1)
+ {
+ // Serial.println("Events");
+
+ activeEuclid->setEvents(map(newValue, 0, 127, 0, 32));
+ }
+ if (potIndex == 2)
+ {
+ // Serial.println("Steps");
+
+ activeEuclid->setSteps(map(newValue, 0, 127, 0, 32));
+ }
+ if (potIndex == 3)
+ {
+ // Serial.println("length");
+
+ uint8_t prevLength = activeEuclid->getNoteLength();
+ uint8_t newLength = map(newValue, 0, 127, 0, kNumNoteLengths - 1);
+
+ activeEuclid->setNoteLength(newLength);
+
+ if (prevLength != newLength)
+ {
+ tempString = String(kNoteLengths[newLength]);
+ omxDisp.displayMessage(tempString.c_str());
+ }
+ }
+ if (potIndex == 4)
+ {
+ // Serial.println("Clock");
+
+ uint8_t prevRes = activeEuclid->getClockDivMult();
+ uint8_t newres = map(newValue, 0, 127, 0, 6);
+ if (polyRhythmMode)
+ {
+ for (u_int8_t i = 0; i < kNumEuclids; i++)
+ {
+ euclids[i].setPolyRClockDivMult(newres);
+ }
+ initEuclid_.polyRClockDivMultP_ = newres;
+ }
+ else
+ {
+ activeEuclid->setClockDivMult(newres);
+ }
+
+ if (newres != prevRes)
+ {
+ tempString = String(multValues[newres]);
+ omxDisp.displayMessage(tempString.c_str());
+ }
+ }
+ }
+
+ omxLeds.setDirty();
+ omxDisp.setDirty();
+
+ // Serial.println((String)"AnalogDelta: " + analogDelta);
+}
+
+void OmxModeEuclidean::loopUpdate(Micros elapsedTime)
+{
+ // if (isSubmodeEnabled())
+ // {
+ // activeSubmode->loopUpdate();
+ // // return;
+ // }
+
+ if (midiModeception)
+ {
+ midiKeyboard.loopUpdate(elapsedTime);
+ // return;
+ }
+
+ if (!isSubmodeEnabled() && !midiModeception)
+ {
+ auto keyState = midiSettings.keyState;
+
+ f1_ = keyState[1] && !keyState[2];
+ f2_ = !keyState[1] && keyState[2];
+ f3_ = keyState[1] && keyState[2];
+ fNone_ = !keyState[1] && !keyState[2];
+ }
+
+ // bool testProb = probResult(sequencer.getCurrentPattern()->steps[sequencer.seqPos[sequencer.playingPattern]].prob);
+
+ // if (sequencer.playing)
+ // {
+ // uint32_t playstepmicros = micros();
+
+ // euclids[0].clockTick(playstepmicros, clockConfig.step_micros);
+ // }
+
+ uint32_t playstepmicros = seqConfig.currentFrameMicros;
+
+ bool clockAdvanced = false;
+
+ for (u_int8_t i = 0; i < kNumEuclids; i++)
+ {
+ euclids[i].clockTick(playstepmicros, clockConfig.step_micros);
+
+ if (euclids[i].getClockAdvanced())
+ {
+ clockAdvanced = true;
+ }
+ }
+
+ if (clockAdvanced)
+ {
+ omxDisp.setDirty();
+ omxLeds.setDirty();
+ }
+
+ for (uint8_t i = 0; i < 5; i++)
+ {
+ // Lets them do things in background
+ subModeMidiFx[i].loopUpdate();
+ }
+}
+
+// void OmxModeEuclidean::setParam(uint8_t pageIndex, uint8_t paramPosition)
+// {
+// int p = pageIndex * NUM_DISP_PARAMS + paramPosition;
+// setParam(p);
+// omxDisp.setDirty();
+// }
+
+// void OmxModeEuclidean::setParam(uint8_t paramIndex)
+// {
+// if (paramIndex >= 0)
+// {
+// param = paramIndex % kNumParams;
+// }
+// else
+// {
+// param = (paramIndex + kNumParams) % kNumParams;
+// }
+// page = param / NUM_DISP_PARAMS;
+
+// // if(instLockView_ && page == GRIDS_DENSITY)
+// // {
+// // int pIndex = param % NUM_DISP_PARAMS;
+// // if(pIndex > 0){
+// // lockedInst_ = pIndex - 1;
+// // }
+// // }
+// }
+
+void OmxModeEuclidean::onEncoderChanged(Encoder::Update enc)
+{
+ if (isSubmodeEnabled())
+ {
+ activeSubmode->onEncoderChanged(enc);
+ return;
+ }
+
+ if (midiModeception)
+ {
+ midiKeyboard.onEncoderChanged(enc);
+ return;
+ }
+
+ // --- EDIT MODE ---
+ if (paramMode_ == PARAMMODE_EDIT)
+ {
+ int8_t selPage = getSelectedParamMode()->getSelPage();
+
+ if (encoderSelect_ || selPage == SELEUCLID_PAT)
+ {
+ onEncoderChangedSelectParam(enc);
+ return;
+ }
+
+ EuclideanSequencer *activeEuclid = &euclids[selectedEuclid_];
+
+ auto amtSlow = enc.accel(1);
+ auto amtFast = enc.accel(5);
+
+ int8_t selParam = getSelectedParamMode()->getSelParam() + 1; // Add one for readability
+
+ switch (selPage)
+ {
+ case SELEUCLID_PAT:
+ {
+ }
+ break;
+ case SELEUCLID_1:
+ {
+ if (selParam == 1)
+ {
+ activeEuclid->setRotation(constrain(activeEuclid->getRotation() + amtSlow, 0, 32));
+ }
+ else if (selParam == 2)
+ {
+ activeEuclid->setEvents(constrain(activeEuclid->getEvents() + amtSlow, 0, 32));
+ }
+ else if (selParam == 3)
+ {
+ activeEuclid->setSteps(constrain(activeEuclid->getSteps() + amtSlow, 0, 32));
+ }
+ else if (selParam == 4)
+ {
+ uint8_t prevLength = activeEuclid->getNoteLength();
+ uint8_t newLength = constrain(prevLength + amtSlow, 0, kNumNoteLengths - 1);
+
+ activeEuclid->setNoteLength(newLength);
+
+ if (prevLength != newLength)
+ {
+ omxDisp.displayMessageTimed(String(kNoteLengths[newLength]), 10);
+ }
+ }
+ }
+ break;
+ case SELEUCLID_NOTES:
+ {
+ if (selParam == 1)
+ {
+ activeEuclid->setNoteNumber(constrain(activeEuclid->getNoteNumber() + amtFast, 0, 127));
+ }
+ else if (selParam == 2)
+ {
+ activeEuclid->setMidiChannel(constrain(activeEuclid->getMidiChannel() + amtSlow, 1, 16));
+ }
+ else if (selParam == 3)
+ {
+ activeEuclid->setVelocity(constrain(activeEuclid->getVelocity() + amtFast, 0, 127));
+ }
+ else if (selParam == 4)
+ {
+ activeEuclid->setSwing(constrain(activeEuclid->getSwing() + amtFast, 0, 100));
+ }
+ }
+ break;
+ case SELEUCLID_CFG1:
+ {
+ if (selParam == 1)
+ {
+ bool prevVal = polyRhythmMode;
+
+ polyRhythmMode = (bool)constrain(polyRhythmMode + amtSlow, 0, 1);
+
+ if (prevVal != polyRhythmMode)
+ {
+ for (u_int8_t i = 0; i < kNumEuclids; i++)
+ {
+ euclids[i].setPolyRhythmMode(polyRhythmMode);
+ }
+
+ initEuclid_.polyRhythmMode_ = polyRhythmMode;
+
+ if (polyRhythmMode)
+ {
+ omxDisp.displayMessage("PolyRhythm");
+ }
+ else
+ {
+ omxDisp.displayMessage("PolyMeter");
+ }
+ }
+ }
+ else if (selParam == 2) // Track Mult
+ {
+ uint8_t prevRes = activeEuclid->getClockDivMult();
+ uint8_t newres = constrain(prevRes + amtSlow, 0, 6);
+
+ if (prevRes != newres)
+ {
+ activeEuclid->setClockDivMult(newres);
+
+ tempString = String(multValues[newres]);
+ omxDisp.displayMessage(tempString.c_str());
+ }
+ }
+ else if (selParam == 3) // Global polyRhythm Mult
+ {
+ uint8_t prevRes = euclids[0].getPolyRClockDivMult();
+ uint8_t newres = constrain(prevRes + amtSlow, 0, 6);
+
+ if (prevRes != newres)
+ {
+ for (u_int8_t i = 0; i < kNumEuclids; i++)
+ {
+ euclids[i].setPolyRClockDivMult(newres);
+ }
+
+ tempString = String(multValues[newres]);
+ omxDisp.displayMessage(tempString.c_str());
+ }
+ }
+ else if (selParam == 4) // BPM
+ {
+ clockConfig.newtempo = constrain(clockConfig.clockbpm + amtFast, 40, 300);
+ if (clockConfig.newtempo != clockConfig.clockbpm)
+ {
+ // SET TEMPO HERE
+ clockConfig.clockbpm = clockConfig.newtempo;
+ omxUtil.resetClocks();
+ }
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ omxLeds.setDirty();
+ omxDisp.setDirty();
+}
+
+// Handles selecting params using encoder
+void OmxModeEuclidean::onEncoderChangedSelectParam(Encoder::Update enc)
+{
+ if (enc.dir() == 0)
+ return;
+
+ if (enc.dir() < 0) // if turn CCW
+ {
+ getSelectedParamMode()->decrementParam();
+ }
+ else if (enc.dir() > 0) // if turn CW
+ {
+ getSelectedParamMode()->incrementParam();
+ }
+
+ omxDisp.setDirty();
+}
+
+void OmxModeEuclidean::onEncoderButtonDown()
+{
+ if (isSubmodeEnabled())
+ {
+ activeSubmode->onEncoderButtonDown();
+ return;
+ }
+
+ if (midiModeception)
+ {
+ midiKeyboard.onEncoderButtonDown();
+ return;
+ }
+
+ int8_t selPage = getSelectedParamMode()->getSelPage();
+
+ // --- EDIT MODE ---
+ if (paramMode_ == PARAMMODE_EDIT)
+ {
+ if (selPage == SELEUCLID_PAT)
+ {
+ encoderSelect_ = true;
+
+ // polyRhythmMode = !polyRhythmMode;
+
+ // for (u_int8_t i = 0; i < kNumEuclids; i++)
+ // {
+ // euclids[i].setPolyRhythmMode(polyRhythmMode);
+ // }
+
+ // if (polyRhythmMode)
+ // {
+ // omxDisp.displayMessage("PolyRhythm");
+ // }
+ // else
+ // {
+ // omxDisp.displayMessage("PolyMeter");
+ // }
+ }
+ else
+ {
+ encoderSelect_ = !encoderSelect_;
+ }
+ }
+ else
+ {
+ if (selPage == SELEUCLID_PAT)
+ {
+ }
+ else
+ {
+ encoderSelect_ = !encoderSelect_;
+ }
+ }
+
+ omxLeds.setDirty();
+ omxDisp.setDirty();
+}
+
+void OmxModeEuclidean::onEncoderButtonDownLong()
+{
+ // if(isSubmodeEnabled()){
+ // activeSubmode->onEncoderButtonDownLong();
+ // return;
+ // }
+
+ if (midiModeception)
+ {
+ midiKeyboard.onEncoderButtonDownLong();
+ return;
+ }
+
+ omxLeds.setDirty();
+ omxDisp.setDirty();
+}
+
+bool OmxModeEuclidean::shouldBlockEncEdit()
+{
+ if (isSubmodeEnabled())
+ {
+ return activeSubmode->shouldBlockEncEdit();
+ }
+
+ if (midiModeception)
+ {
+ return midiKeyboard.shouldBlockEncEdit();
+ }
+
+ return false;
+}
+
+void OmxModeEuclidean::saveActivePattern(uint8_t pattIndex, bool showMsg)
+{
+ for (uint8_t i = 0; i < kNumEuclids; i++)
+ {
+ saveSlots_[pattIndex].euclids[i] = euclids[i].getSave();
+ }
+
+ saveSlots_[pattIndex].polyRhythmMode_ = polyRhythmMode;
+ selectedSave_ = pattIndex;
+
+ if (showMsg)
+ {
+ omxDisp.displayMessageTimed("Saved " + String(pattIndex + 1), 5);
+ }
+}
+
+void OmxModeEuclidean::loadActivePattern(uint8_t pattIndex)
+{
+ for (uint8_t i = 0; i < kNumEuclids; i++)
+ {
+ euclids[i].loadSave(saveSlots_[pattIndex].euclids[i]);
+ }
+
+ polyRhythmMode = saveSlots_[pattIndex].polyRhythmMode_;
+ selectedSave_ = pattIndex;
+
+ omxDisp.displayMessageTimed("Load " + String(pattIndex + 1), 5);
+}
+
+void OmxModeEuclidean::onKeyUpdate(OMXKeypadEvent e)
+{
+ omxLeds.setDirty();
+
+ if (isSubmodeEnabled())
+ {
+ if (activeSubmode->onKeyUpdate(e))
+ return;
+ }
+
+ int thisKey = e.key();
+
+ if (midiModeception)
+ {
+ midiKeyboard.onKeyUpdate(e);
+
+ if (midiSettings.keyState[0] && e.down() && thisKey == 26)
+ {
+ midiModeception = false;
+ midiSettings.midiAUX = false;
+ omxDisp.setDirty();
+ omxLeds.setDirty();
+ }
+
+ return;
+ }
+
+ EuclideanSequencer *activeEuclid = &euclids[selectedEuclid_];
+
+ // if (instLockView_)
+ // {
+ // onKeyUpdateChanLock(e);
+ // return;
+ // }
+ // // auto keyState = midiSettings.keyState;
+ if (!e.held())
+ {
+ if (e.down() && thisKey == 0) // Aux key down
+ {
+ // Sequencer shouldn't be a dependancy here but current is used to advance clocks.
+ if (isPlaying_ && aux_)
+ {
+ aux_ = false;
+ stopSequencers();
+ // sequencer.playing = false;
+ }
+ else
+ {
+ aux_ = true;
+ startSequencers();
+ // sequencer.playing = true;
+ }
+ }
+ // else if (e.down() && e.clicks() == 0 && (thisKey > 2 && thisKey < 11))
+ // {
+ // int patt = thisKey - 3;
+
+ // if (f2_)
+ // {
+ // saveActivePattern(patt);
+ // }
+ // else if(fNone_)
+ // {
+ // loadActivePattern(patt);
+ // }
+ // }
+ }
+
+ if (e.down() && thisKey == 3)
+ {
+ setParamMode(PARAMMODE_MIX);
+ }
+ else if (e.down() && thisKey == 4)
+ {
+ setParamMode(PARAMMODE_EDIT);
+ }
+ else if (e.down() && thisKey == 5)
+ {
+ setParamMode(PARAMMODE_PATTERN);
+ }
+
+ // --- EDIT MODE ---
+ if (paramMode_ == PARAMMODE_EDIT || paramMode_ == PARAMMODE_MIX)
+ {
+ if (fNone_)
+ {
+ if (e.down() && (thisKey > 10) && thisKey < 19)
+ {
+ selectEuclid(thisKey - 11);
+
+ if (paramMode_ == PARAMMODE_MIX)
+ {
+ toggleMute(thisKey - 11);
+ }
+
+ copiedEuclid_ = euclids[thisKey - 11].getSave();
+ }
+
+ if (e.down() && thisKey >= 6 && thisKey < 11)
+ {
+ activeEuclid->midiFXGroup = thisKey - 6;
+ // enableSubmode(&subModeMidiFx[thisKey - 8]);
+ }
+
+ if (!e.down() && e.clicks() == 2 && thisKey >= 6 && thisKey < 11)
+ {
+ enableSubmode(&subModeMidiFx[thisKey - 6]);
+ }
+ }
+ else if (f1_) // Mute
+ {
+ if (e.down() && (thisKey > 10) && thisKey < 19)
+ {
+ toggleMute(thisKey - 11);
+ }
+ }
+ else if (f2_) // Paste
+ {
+ if (e.down() && (thisKey > 10) && thisKey < 19)
+ {
+ euclids[thisKey - 11].loadSave(copiedEuclid_);
+ omxDisp.displayMessageTimed("Paste: " + String(thisKey - 11 + 1), 5);
+ }
+ }
+ else if (f3_) // Cut
+ {
+ if (e.down() && (thisKey > 10) && thisKey < 19)
+ {
+ selectEuclid(thisKey - 11);
+ copiedEuclid_ = euclids[thisKey - 11].getSave();
+ euclids[thisKey - 11].loadSave(initEuclid_);
+ omxDisp.displayMessageTimed("Cut: " + String(thisKey - 11 + 1), 5);
+ }
+ }
+ }
+ // --- PATTERN MODE ---
+ else if (paramMode_ == PARAMMODE_PATTERN)
+ {
+ if (f2_)
+ {
+ if (e.down() && e.clicks() == 0 && thisKey > 10)
+ {
+ uint8_t patt = thisKey - 11;
+
+ saveActivePattern(patt);
+ }
+ }
+ else
+ {
+ if (e.down() && e.clicks() == 0 && thisKey > 10)
+ {
+ uint8_t patt = thisKey - 11;
+
+ loadActivePattern(patt);
+ }
+ }
+ }
+ omxDisp.setDirty();
+}
+
+void OmxModeEuclidean::toggleMute(uint8_t euclidIndex)
+{
+ bool muted = !euclids[euclidIndex].getMute();
+ euclids[euclidIndex].setMute(muted);
+
+ omxDisp.displayMessageTimed(String(euclidIndex + 1) + (muted ? " Muted" : " Unmuted"), 5);
+
+ omxLeds.setDirty();
+ omxDisp.setDirty();
+}
+
+void OmxModeEuclidean::selectEuclid(uint8_t euclidIndex)
+{
+ // if(instLockView_ && lockedInst_ == instIndex) return;
+
+ // instLockView_ = true;
+ // // justLocked_ = true; // Uncomment to immediately switch to channel view
+ // lockedInst_ = instIndex;
+
+ // if (page == GRIDS_DENSITY || page == GRIDS_NOTES)
+ // {
+ // setParam(page, lockedInst_ + 1);
+ // }
+
+ selectedEuclid_ = euclidIndex;
+
+ // omxDisp.displayMessage((String) "Euclid " + (euclidIndex + 1));
+ omxLeds.setDirty();
+ omxDisp.setDirty();
+}
+
+void OmxModeEuclidean::onKeyHeldUpdate(OMXKeypadEvent e)
+{
+ if (isSubmodeEnabled())
+ {
+ if (activeSubmode->onKeyHeldUpdate(e))
+ return;
+ }
+
+ if (midiModeception)
+ {
+ midiKeyboard.onKeyHeldUpdate(e);
+ return;
+ }
+
+ int thisKey = e.key();
+
+ // --- EDIT MODE ---
+ if (paramMode_ == PARAMMODE_EDIT || paramMode_ == PARAMMODE_MIX)
+ {
+ // Enter MidiFX mode
+ if (thisKey >= 6 && thisKey < 11)
+ {
+ enableSubmode(&subModeMidiFx[thisKey - 6]);
+ }
+ }
+
+ omxLeds.setDirty();
+ omxDisp.setDirty();
+}
+
+void OmxModeEuclidean::updateLEDs()
+{
+ if (isSubmodeEnabled())
+ {
+ if (activeSubmode->updateLEDs())
+ return;
+ }
+
+ // Serial.println("Euclidean Leds");
+
+ if (midiModeception)
+ {
+ return;
+ }
+
+ // omxLeds.updateBlinkStates();
+ EuclideanSequencer *activeEuclid = &euclids[selectedEuclid_];
+
+ bool blinkState = omxLeds.getBlinkState();
+
+ // turn leds off
+ for (uint8_t i = 1; i < 27; i++)
+ {
+ strip.setPixelColor(i, LEDOFF);
+ }
+
+ if (isPlaying_)
+ {
+ // Blink left/right keys for octave select indicators.
+ auto color1 = blinkState ? LIME : LEDOFF;
+ strip.setPixelColor(0, color1);
+ }
+ else
+ {
+ strip.setPixelColor(0, LEDOFF);
+ }
+
+ // Function Keys
+ if (f3_)
+ {
+ auto f3Color = blinkState ? LEDOFF : FUNKTHREE;
+ strip.setPixelColor(1, f3Color);
+ strip.setPixelColor(2, f3Color);
+ }
+ else
+ {
+ auto f1Color = (f1_ && blinkState) ? LEDOFF : FUNKONE;
+ strip.setPixelColor(1, f1Color);
+
+ auto f2Color = (f2_ && blinkState) ? LEDOFF : FUNKTWO;
+ strip.setPixelColor(2, f2Color);
+ }
+
+ strip.setPixelColor(3, paramMode_ == PARAMMODE_MIX ? WHITE : kMixColor);
+ strip.setPixelColor(4, paramMode_ == PARAMMODE_EDIT ? WHITE : kEuclidColor);
+ strip.setPixelColor(5, paramMode_ == PARAMMODE_PATTERN ? WHITE : kSaveColor);
+
+ // --- EDIT MODE ---
+ if (paramMode_ == PARAMMODE_MIX)
+ {
+ for (uint8_t i = 0; i < NUM_MIDIFX_GROUPS; i++)
+ {
+ auto mfxColor = (i == activeEuclid->midiFXGroup) ? kSelMidiFXColor : kMidiFXColor;
+
+ strip.setPixelColor(6 + i, mfxColor);
+ }
+
+ for (uint8_t i = 0; i < kNumEuclids; i++)
+ {
+ auto eucColor = euclids[i].getMute() ? kMixMuteColor : kMixColor;
+ if (isPlaying_)
+ {
+ eucColor = euclids[i].getTriggered() ? kMixTrigger : eucColor;
+ }
+ strip.setPixelColor(11 + i, eucColor);
+ }
+ }
+ else if (paramMode_ == PARAMMODE_EDIT)
+ {
+ for (uint8_t i = 0; i < NUM_MIDIFX_GROUPS; i++)
+ {
+ auto mfxColor = (i == activeEuclid->midiFXGroup) ? kSelMidiFXColor : kMidiFXColor;
+
+ strip.setPixelColor(6 + i, mfxColor);
+ }
+
+ for (uint8_t i = 0; i < kNumEuclids; i++)
+ {
+ auto eucColor = euclids[i].getMute() ? kEuclidMuteColor : kEuclidColor;
+ if (isPlaying_)
+ {
+ eucColor = euclids[i].getTriggered() ? kEuclidTrigger : eucColor;
+ }
+ if (i == selectedEuclid_)
+ {
+ eucColor = euclids[i].getMute() ? kSelEuclidMuteColor : kSelEuclidColor;
+ eucColor = euclids[i].getTriggered() ? kSelEuclidTriggerColor : eucColor;
+ }
+ strip.setPixelColor(11 + i, eucColor);
+ }
+ }
+ else if (paramMode_ == PARAMMODE_PATTERN)
+ {
+ for (uint8_t i = 0; i < kNumSaves; i++)
+ {
+ auto saveColor = (i == selectedSave_) ? kSelSaveColor : kSaveColor;
+ strip.setPixelColor(11 + i, saveColor);
+ }
+ }
+
+ if (isSubmodeEnabled())
+ {
+ bool blinkStateSlow = omxLeds.getSlowBlinkState();
+
+ auto auxColor = (blinkStateSlow ? RED : LEDOFF);
+ strip.setPixelColor(0, auxColor);
+ }
+}
+
+void OmxModeEuclidean::updateLEDsFNone()
+{
+ // bool blinkState = omxLeds.getBlinkState();
+
+ // auto keyState = midiSettings.keyState;
+
+ // for (int k = 0; k < 4; k++)
+ // {
+ // // Change color of 4 GridX keys when pushed
+ // // auto kColor = keyState[k + 11] ? (blinkState ? paramSelColors[k] : LEDOFF) : PINK;
+ // auto kColor = keyState[k + 11] ? (blinkState ? paramSelColors[k] : LEDOFF) : BLUE;
+
+ // strip.setPixelColor(k + 11, kColor);
+ // }
+
+ // for (int k = 4; k < 8; k++)
+ // {
+ // // Change color of 4 GridY keys when pushed
+ // // auto kColor = keyState[k + 11] ? (blinkState ? paramSelColors[k % 4] : LEDOFF) : GREEN;
+ // auto kColor = keyState[k + 11] ? (blinkState ? paramSelColors[k % 4] : LEDOFF) : LTCYAN;
+ // strip.setPixelColor(k + 11, kColor);
+ // }
+
+ // for (int k = 0; k < 4; k++)
+ // {
+ // bool triggered = grids_.getChannelTriggered(k);
+ // // Change color of 4 GridY keys when pushed
+ // auto kColor = triggered ? paramSelColors[k] : LEDOFF;
+ // strip.setPixelColor(k + 19, kColor);
+ // }
+
+ // strip.setPixelColor(23, (keyState[23] ? LBLUE : BLUE)); // Accent
+ // strip.setPixelColor(24, (keyState[24] ? WHITE : ORANGE)); // Xaos
+ // strip.setPixelColor(26, (keyState[26] ? WHITE : MAGENTA)); // BPM
+}
+
+void OmxModeEuclidean::updateLEDsF1()
+{
+ // bool blinkState = omxLeds.getBlinkState();
+ // auto keyState = midiSettings.keyState;
+
+ // // updateLEDsChannelView();
+
+ // for (int k = 0; k < 4; k++)
+ // {
+ // // Change color of 4 GridX keys when pushed
+ // auto kColor = keyState[k + 11] ? (blinkState ? paramSelColors[k] : LEDOFF) : ORANGE;
+ // strip.setPixelColor(k + 11, kColor);
+ // }
+
+ // for (int k = 4; k < 8; k++)
+ // {
+ // strip.setPixelColor(k + 11, LEDOFF);
+ // }
+
+ // strip.setPixelColor(26, ORANGE);
+}
+
+void OmxModeEuclidean::updateLEDsPatterns()
+{
+ // int patternNum = grids_.playingPattern;
+
+ // // LEDS for top row
+ // for (int j = 3; j < LED_COUNT - 16; j++)
+ // {
+ // auto pColor = (j == patternNum + 3) ? seqColors[patternNum] : LEDOFF;
+ // strip.setPixelColor(j, pColor);
+ // }
+}
+
+// Called by pending note offs when a pending note off is sent
+void OmxModeEuclidean::onPendingNoteOff(int note, int channel)
+{
+ // Serial.println("OmxModeEuclidean::onPendingNoteOff " + String(note) + " " + String(channel));
+ // subModeMidiFx.onPendingNoteOff(note, channel);
+
+ for (uint8_t i = 0; i < NUM_MIDIFX_GROUPS; i++)
+ {
+ subModeMidiFx[i].onPendingNoteOff(note, channel);
+ }
+}
+
+// Called by a euclid sequencer when it triggers a note
+void OmxModeEuclidean::onNoteTriggered(uint8_t euclidIndex, MidiNoteGroup note)
+{
+ // Serial.println("OmxModeEuclidean::onNoteTriggered " + String(euclidIndex) + " note: " + String(note.noteNumber));
+
+ uint8_t mfxIndex = euclids[euclidIndex].midiFXGroup;
+
+ subModeMidiFx[mfxIndex].noteInput(note);
+
+ omxDisp.setDirty();
+ omxLeds.setDirty();
+}
+
+// Called by the midiFX group when a note exits it's FX Pedalboard
+void OmxModeEuclidean::onNotePostFX(MidiNoteGroup note)
+{
+ if (note.noteOff)
+ {
+ // Serial.println("onNotePostFX note off: " + String(note.noteNumber));
+ pendingNoteOns.remove(note.noteNumber, note.channel);
+ pendingNoteOffs.sendOffNow(note.noteNumber, note.channel, note.sendCV);
+ }
+ else
+ {
+ // Serial.println("onNotePostFX note on: " + String(note.noteNumber));
+
+ // Serial.println("OmxModeEuclidean::onNotePostFX note: " + String(note.noteNumber));
+
+ uint32_t noteOnMicros = note.noteonMicros; // TODO Might need to be set to current micros
+ pendingNoteOns.insert(note.noteNumber, note.velocity, note.channel, noteOnMicros, note.sendCV);
+
+ // MM::sendNoteOn(note.noteNumber, note.velocity, note.channel);
+
+ // uint32_t noteOnMicros = seqConfig.currentFrameMicros; // TODO Might need to be set to current micros
+
+ uint32_t noteOffMicros = noteOnMicros + (note.stepLength * clockConfig.step_micros);
+ pendingNoteOffs.insert(note.noteNumber, note.channel, noteOffMicros, note.sendCV);
+ }
+
+ // Serial.println("\n\n");
+}
+
+void OmxModeEuclidean::setupPageLegends()
+{
+ omxDisp.clearLegends();
+
+ int8_t page = getSelectedParamMode()->getSelPage();
+
+ EuclideanSequencer *activeEuclid = &euclids[selectedEuclid_];
+
+ switch (page)
+ {
+ case SELEUCLID_1:
+ {
+ omxDisp.legends[0] = "ROT";
+ omxDisp.legends[1] = "EVTS";
+ omxDisp.legends[2] = "STEPS";
+ omxDisp.legends[3] = "LEN";
+ omxDisp.legendVals[0] = activeEuclid->getRotation();
+ omxDisp.legendVals[1] = activeEuclid->getEvents();
+ omxDisp.legendVals[2] = activeEuclid->getSteps();
+ omxDisp.legendVals[3] = activeEuclid->getNoteLength();
+ }
+ break;
+ case SELEUCLID_NOTES:
+ {
+ omxDisp.legends[0] = "NOTE";
+ omxDisp.legends[1] = "CHAN";
+ omxDisp.legends[2] = "VEL";
+ omxDisp.legends[3] = "SWNG";
+ omxDisp.legendVals[0] = activeEuclid->getNoteNumber();
+ omxDisp.legendVals[1] = activeEuclid->getMidiChannel();
+ omxDisp.legendVals[2] = activeEuclid->getVelocity();
+ omxDisp.legendVals[3] = activeEuclid->getSwing();
+ }
+ break;
+ case SELEUCLID_CFG1:
+ {
+ omxDisp.legends[0] = "MODE";
+ omxDisp.legends[1] = "TRAT";
+ omxDisp.legends[2] = "PRAT";
+ omxDisp.legends[3] = "BPM";
+ omxDisp.legendVals[0] = (int)polyRhythmMode;
+ omxDisp.useLegendString[1] = true;
+ omxDisp.legendString[1] = String(activeEuclid->getClockDivMult());
+ omxDisp.useLegendString[2] = true;
+ omxDisp.legendString[2] = String(euclids[0].getPolyRClockDivMult());
+ omxDisp.legendVals[3] = (int)clockConfig.clockbpm;
+ }
+ break;
+ default:
+ break;
+ }
+}
+
+void OmxModeEuclidean::onDisplayUpdate()
+{
+ if (isSubmodeEnabled())
+ {
+ if (omxLeds.isDirty())
+ {
+ updateLEDs();
+ }
+
+ activeSubmode->onDisplayUpdate();
+ return;
+ }
+
+ if (midiModeception)
+ {
+ midiKeyboard.onDisplayUpdate();
+
+ if (midiSettings.midiAUX)
+ {
+ strip.setPixelColor(26, RED); // Highlight aux exit key
+ }
+
+ return;
+ }
+
+ // omxLeds.updateBlinkStates();
+
+ if (omxLeds.isDirty())
+ {
+ updateLEDs();
+ }
+
+ if (omxDisp.isDirty())
+ {
+ if (!encoderConfig.enc_edit)
+ {
+ auto params = getSelectedParamMode();
+
+ if (!fNone_ && (paramMode_ == PARAMMODE_EDIT || paramMode_ == PARAMMODE_MIX))
+ {
+ if (f1_)
+ {
+ omxDisp.dispGenericModeLabel("Mute", params->getNumPages(), params->getSelPage());
+ }
+ else if (f2_)
+ {
+ omxDisp.dispGenericModeLabel("Paste", params->getNumPages(), params->getSelPage());
+ }
+ else if (f3_)
+ {
+ omxDisp.dispGenericModeLabel("Cut", params->getNumPages(), params->getSelPage());
+ }
+ }
+ else if (paramMode_ == PARAMMODE_PATTERN)
+ {
+ if (f2_)
+ {
+ omxDisp.dispGenericModeLabel("Save To", 0, 0);
+ }
+ else
+ {
+ omxDisp.dispGenericModeLabel("Load From", 0, 0);
+ }
+ }
+ else
+ {
+ if (params->getSelPage() == SELEUCLID_PAT)
+ {
+ // if (isPlaying_)
+ // {
+ // omxDisp.setDirty();
+ // }
+
+ // for (uint8_t i = 0; i < 4; i++)
+ // {
+ // uint8_t ypos = 7 * (i + 1);
+ // bool selected = i == selectedEuclid_;
+ // omxDisp.drawEuclidPattern(euclids[i].getPattern(), euclids[i].getSteps(), ypos, selected, euclids[i].isRunning(), euclids[i].getLastSeqPos());
+ // }
+
+ EuclideanSequencer *activeEuclid = &euclids[selectedEuclid_];
+
+ uint8_t ypos = 20;
+
+ omxDisp.drawEuclidPattern(true, activeEuclid->getPattern(), activeEuclid->getSteps(), ypos, false, activeEuclid->isRunning(), activeEuclid->getLastSeqPos());
+
+ omxDisp.dispPageIndicators2(params->getNumPages(), 0);
+
+ // for(int i = 0; i < 4; i++){
+
+ // bool selected = i == 0;
+
+ // omxDisp.dispPageIndicators(i, selected);
+ // }
+
+ // int pselected = param % NUM_DISP_PARAMS;
+ // setupPageLegends();
+ // omxDisp.dispGenericMode(pselected);
+ }
+ else
+ {
+ setupPageLegends();
+ omxDisp.dispGenericMode2(params->getNumPages(), params->getSelPage(), params->getSelParam(), encoderSelect_);
+ }
+ }
+ }
+ }
+
+ // if (!encoderConfig.enc_edit)
+ // {
+ // omxDisp.drawEuclidPattern(euclids[0].getPattern(), euclids[0].getSteps());
+ // // int pselected = param % NUM_DISP_PARAMS;
+ // // setupPageLegends();
+ // // omxDisp.dispGenericMode(pselected);
+ // }
+
+ // if (!encoderConfig.enc_edit)
+ // {
+ // omxDisp.drawEuclidPattern(euclids[0].getPattern() , euclids[0].getSteps());
+ // // int pselected = param % NUM_DISP_PARAMS;
+ // // setupPageLegends();
+ // // omxDisp.dispGenericMode(pselected);
+ // }
+}
+
+void OmxModeEuclidean::SetScale(MusicScales *scale)
+{
+ midiKeyboard.SetScale(scale);
+}
+
+void OmxModeEuclidean::enableSubmode(SubmodeInterface *subMode)
+{
+ activeSubmode = subMode;
+ activeSubmode->setEnabled(true);
+ omxDisp.setDirty();
+}
+
+void OmxModeEuclidean::disableSubmode()
+{
+ activeSubmode = nullptr;
+ omxDisp.setDirty();
+}
+
+bool OmxModeEuclidean::isSubmodeEnabled()
+{
+ if (activeSubmode == nullptr)
+ return false;
+
+ if (activeSubmode->isEnabled() == false)
+ {
+ disableSubmode();
+ return false;
+ }
+
+ return true;
+}
+
+// int OmxModeGrids::serializedPatternSize(bool eeprom)
+// {
+// return sizeof(grids::SnapShotSettings);
+// }
+
+// grids::SnapShotSettings* OmxModeGrids::getPattern(uint8_t patternIndex)
+// {
+// return grids_.getSnapShot(patternIndex);
+// }
+
+// void OmxModeGrids::setPattern(uint8_t patternIndex, grids::SnapShotSettings snapShot)
+// {
+// grids_.setSnapShot(patternIndex, snapShot);
+// }
+
+int OmxModeEuclidean::saveToDisk(int startingAddress, Storage *storage)
+{
+ storage->write(startingAddress, selectedSave_);
+ startingAddress++;
+
+ int saveSize = sizeof(EuclidPatternSave);
+
+ for (uint8_t i = 0; i < kNumSaves; i++)
+ {
+ auto saveBytesPtr = (byte *)(&saveSlots_[i]);
+ for (int j = 0; j < saveSize; j++)
+ {
+ storage->write(startingAddress + j, *saveBytesPtr++);
+ }
+
+ startingAddress += saveSize;
+ }
+
+ return startingAddress;
+}
+
+int OmxModeEuclidean::loadFromDisk(int startingAddress, Storage *storage)
+{
+ selectedSave_ = storage->read(startingAddress);
+ startingAddress++;
+
+ int saveSize = sizeof(EuclidPatternSave);
+
+ for (uint8_t i = 0; i < kNumSaves; i++)
+ {
+ auto pattern = EuclidPatternSave{};
+ auto current = (byte *)&pattern;
+ for (int j = 0; j < saveSize; j++)
+ {
+ *current = storage->read(startingAddress + j);
+ current++;
+ }
+
+ saveSlots_[i] = pattern;
+ startingAddress += saveSize;
+ }
+
+ // Load selected save to active
+ for (uint8_t i = 0; i < kNumEuclids; i++)
+ {
+ euclids[i].loadSave(saveSlots_[selectedSave_].euclids[i]);
+ }
+
+ polyRhythmMode = saveSlots_[selectedSave_].polyRhythmMode_;
+
+ return startingAddress;
+}
diff --git a/Archive/OMX-27-firmware/src/modes/omx_mode_euclidean.h b/Archive/OMX-27-firmware/src/modes/omx_mode_euclidean.h
new file mode 100644
index 00000000..55f38de9
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/modes/omx_mode_euclidean.h
@@ -0,0 +1,180 @@
+#pragma once
+
+#include "omx_mode_interface.h"
+#include "../utils/music_scales.h"
+#include "../consts/colors.h"
+#include "../config.h"
+#include "omx_mode_midi_keyboard.h"
+#include "euclidean_sequencer.h"
+#include "submodes/submode_midifxgroup.h"
+#include "../utils/param_manager.h"
+
+struct EuclidPatternSave
+{
+ euclidean::EuclidSave euclids[8];
+ bool polyRhythmMode_ = true;
+};
+
+class OmxModeEuclidean : public OmxModeInterface
+{
+public:
+ OmxModeEuclidean();
+ ~OmxModeEuclidean() {}
+
+ void InitSetup() override;
+
+ void onModeActivated() override;
+ void onModeDeactivated() override;
+
+ void onClockTick() override;
+
+ void onPotChanged(int potIndex, int prevValue, int newValue, int analogDelta) override;
+
+ void loopUpdate(Micros elapsedTime) override;
+
+ void updateLEDs() override;
+
+ void onEncoderChanged(Encoder::Update enc) override;
+ void onEncoderButtonDown() override;
+ void onEncoderButtonDownLong() override;
+
+ bool shouldBlockEncEdit() override;
+
+ void onKeyUpdate(OMXKeypadEvent e) override;
+ void onKeyHeldUpdate(OMXKeypadEvent e) override;
+
+ void onDisplayUpdate() override;
+ void SetScale(MusicScales *scale);
+
+ static const u_int8_t kNumEuclids = 8;
+ static const u_int8_t kNumSaves = 16;
+ // static const u_int8_t kNumMidiFXGroups = 5;
+
+ int saveToDisk(int startingAddress, Storage *storage);
+ int loadFromDisk(int startingAddress, Storage *storage);
+
+ // static int serializedPatternSize(bool eeprom);
+ // static inline int getNumPatterns() { return 8; }
+ // grids::SnapShotSettings* getPattern(uint8_t patternIndex);
+ // void setPattern(uint8_t patternIndex, grids::SnapShotSettings snapShot);
+private:
+ // void setParam(uint8_t pageIndex, uint8_t paramPosition);
+ // void setParam(uint8_t paramIndex);
+ void setupPageLegends();
+
+ void updateLEDsFNone();
+ void updateLEDsF1();
+ void updateLEDsPatterns();
+
+ void updateLEDsChannelView();
+ void onKeyUpdateChanLock(OMXKeypadEvent e);
+
+ void saveActivePattern(uint8_t pattIndex, bool showMsg = true);
+ void loadActivePattern(uint8_t pattIndex);
+
+ void toggleMute(uint8_t euclidIndex);
+ void selectEuclid(uint8_t euclidIndex);
+
+ bool initSetup = false;
+
+ bool isPlaying_ = false;
+
+ // String tempString;
+
+ static const uint8_t kNumPages = 4;
+ static const uint8_t kNumParams = kNumPages * NUM_DISP_PARAMS;
+ static const uint8_t kNumGrids = 4;
+
+ uint32_t paramSelColors[4] = {MAGENTA, ORANGE, RED, RBLUE};
+
+ const char *rateNames[3] = {"1 / 2", "1", "2"};
+
+ ParamManager *getSelectedParamMode();
+ void setParamMode(uint8_t newParamMode);
+ void setPageAndParam(uint8_t pageIndex, uint8_t paramPosition, bool editParam);
+ void setParam(uint8_t paramIndex);
+ void onEncoderChangedSelectParam(Encoder::Update enc);
+
+ bool encoderSelect_ = false;
+
+ // ParamManager selEucParams;
+
+ uint8_t paramMode_ = 0;
+
+ ParamManager params_[3];
+
+ uint8_t selectedEuclid_ = 0;
+
+ EuclidPatternSave saveSlots_[kNumSaves];
+ euclidean::EuclidSave copiedEuclid_;
+ euclidean::EuclidSave initEuclid_;
+
+ uint8_t selectedSave_ = 0;
+
+ // int sizeSaves = sizeof(saveSlots_);
+
+ // bool gridsSelected[4] = {false,false,false,false};
+
+ bool aux_ = false;
+
+ bool f1_;
+ bool f2_;
+ bool f3_;
+ bool fNone_;
+
+ bool midiModeception = false;
+ OmxModeMidiKeyboard midiKeyboard; // Mode inside a mode. For science!
+
+ bool pendingStart_ = false;
+
+ bool euclidPattern[32];
+ bool polyRhythmMode = false;
+
+ // u_int8_t rotation;
+ // u_int8_t events;
+ // u_int8_t steps;
+
+ // void drawEuclidPattern(bool* pattern, uint8_t steps);
+
+ // void printEuclidPattern(bool* pattern, uint8_t steps);
+
+ void startSequencers();
+ void stopSequencers();
+
+ euclidean::EuclideanSequencer euclids[kNumEuclids];
+
+ // int esize = sizeof(euclids);
+
+ // SubModes
+ SubmodeInterface *activeSubmode = nullptr;
+
+ // SubModeMidiFxGroup subModeMidiFx[kNumMidiFXGroups];
+
+ void enableSubmode(SubmodeInterface *subMode);
+ void disableSubmode();
+ bool isSubmodeEnabled();
+
+ // Static glue to link a pointer to a member function
+ static void onPendingNoteOffForwarder(void *context, int note, int channel)
+ {
+ static_cast(context)->onPendingNoteOff(note, channel);
+ }
+
+ void onPendingNoteOff(int note, int channel);
+
+ // Static glue to link a pointer to a member function
+ static void onNoteTriggeredForwarder(void *context, uint8_t euclidIndex, MidiNoteGroup note)
+ {
+ static_cast(context)->onNoteTriggered(euclidIndex, note);
+ }
+
+ void onNoteTriggered(uint8_t euclidIndex, MidiNoteGroup note);
+
+ // Static glue to link a pointer to a member function
+ static void onNotePostFXForwarder(void *context, MidiNoteGroup note)
+ {
+ static_cast(context)->onNotePostFX(note);
+ }
+
+ void onNotePostFX(MidiNoteGroup note);
+};
diff --git a/Archive/OMX-27-firmware/src/modes/omx_mode_grids.cpp b/Archive/OMX-27-firmware/src/modes/omx_mode_grids.cpp
new file mode 100644
index 00000000..cf929390
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/modes/omx_mode_grids.cpp
@@ -0,0 +1,1411 @@
+#include "omx_mode_grids.h"
+#include "../config.h"
+#include "../utils/omx_util.h"
+#include "../hardware/omx_disp.h"
+#include "../hardware/omx_leds.h"
+// #include "../modes/sequencer.h"
+#include "../midi/noteoffs.h"
+#include "../consts/consts.h"
+
+using namespace grids;
+
+enum GridModePage
+{
+ GRIDS_DENSITY,
+ GRIDS_XY,
+ GRIDS_NOTES,
+ GRIDS_CONFIG,
+ GRIDS_CONFIG2
+};
+
+OmxModeGrids::OmxModeGrids()
+{
+ grids_.setNoteOutputFunc(&OmxModeGrids::onNoteTriggeredForwarder, this);
+
+ // for (int i = 0; i < 4; i++)
+ // {
+ // gridsXY[i][0] = grids_.getX(i);
+ // gridsXY[i][1] = grids_.getY(i);
+ // }
+ midiKeyboard.setMidiMode();
+
+ // 4 pages
+ params.addPage(4);
+ params.addPage(4);
+ params.addPage(4);
+ params.addPage(4);
+ params.addPage(1);
+}
+
+void OmxModeGrids::InitSetup()
+{
+ initSetup = true;
+}
+
+void OmxModeGrids::onModeActivated()
+{
+ if (!initSetup)
+ {
+ InitSetup();
+ }
+
+ isPlaying_ = false;
+ // sequencer.playing = false;
+ grids_.stop();
+ grids_.loadSnapShot(grids_.playingPattern);
+ for (uint8_t i = 0; i < NUM_CC_POTS; i++)
+ {
+ potPostLoadThresh[i] = true;
+ }
+ gridsAUX = false;
+
+ params.setSelPageAndParam(0, 0);
+ encoderSelect = true;
+}
+
+void OmxModeGrids::onModeDeactivated()
+{
+ stopPlayback();
+}
+
+void OmxModeGrids::onClockTick()
+{
+ grids_.gridsTick();
+}
+
+void OmxModeGrids::onPotChanged(int potIndex, int prevValue, int newValue, int analogDelta)
+{
+#if T4
+ int deltaTheshold = 1;
+#else
+ int deltaTheshold = 6;
+#endif
+
+ if (midiModeception)
+ {
+ midiKeyboard.onPotChanged(potIndex, prevValue, newValue, analogDelta);
+ return;
+ }
+ // Serial.println((String)"AnalogDelta: " + analogDelta);
+
+ // if (analogDelta < 3)
+ // return;
+
+#if T4
+// prevents values from being modified until pot is modified
+ if (potPostLoadThresh[potIndex])
+ {
+ int delta = newValue - prevValue;
+
+ if (delta >= 1)
+ {
+ potPostLoadThresh[potIndex] = false;
+ }
+ else
+ {
+ return;
+ }
+ }
+#else
+ // prevents values from being modified until pot is modified
+ if (potPostLoadThresh[potIndex])
+ {
+ if (analogDelta < deltaTheshold)
+ {
+ return;
+ }
+ else
+ {
+ potPostLoadThresh[potIndex] = false;
+ }
+ }
+#endif
+
+ if (potIndex < 4)
+ {
+ const uint16_t magicPotNumber = 16383;
+ uint8_t singleHighresVal = 64; // magicPotNumber / 256
+ uint8_t prevDensity = grids_.getDensity(potIndex);
+ // uint16_t hiResVal = map(potSettings.hiResPotVal[potIndex], potMinVal, potMaxVal, 0, magicPotNumber);
+ uint16_t hiResVal = potSettings.hiResPotVal[potIndex];
+ uint8_t newDensity = map(hiResVal, 0, magicPotNumber, 0, 255);
+
+ const uint8_t stickyRange = 12;
+
+ // Make value stick to center and sides
+ if (newDensity <= 127)
+ {
+ hiResVal = constrain(hiResVal, (singleHighresVal * 3), (magicPotNumber / 2) - (singleHighresVal * stickyRange));
+ newDensity = map(hiResVal, (singleHighresVal * 3), (magicPotNumber / 2) - (singleHighresVal * stickyRange), 0, 127);
+ }
+ else
+ {
+ hiResVal = constrain(hiResVal, (magicPotNumber / 2) + (singleHighresVal * stickyRange), magicPotNumber - (singleHighresVal * 3));
+ newDensity = map(hiResVal, (magicPotNumber / 2) + (singleHighresVal * stickyRange), magicPotNumber - (singleHighresVal * 3), 127, 255);
+ }
+
+ if (newDensity != prevDensity)
+ {
+ grids_.setDensity(potIndex, newDensity);
+
+ if (analogDelta >= deltaTheshold)
+ {
+ if (params.getSelPage() == GRIDS_DENSITY)
+ {
+ setParam(potIndex);
+ // setParam(GRIDS_DENSITY, potIndex + 1);
+ }
+ }
+
+ omxDisp.setDirty();
+ }
+ }
+ else if (potIndex == 4)
+ {
+ int newres = map(newValue, 0, 127, 0, 2);
+ grids_.setResolution(newres);
+ if (newres != prevResolution_)
+ {
+ omxDisp.displayMessage(rateNames[newres]);
+ }
+ prevResolution_ = newres;
+
+ // if (analogDelta >= 10)
+ // {
+
+ // }
+ }
+}
+
+void OmxModeGrids::loopUpdate(Micros elapsedTime)
+{
+ // uint32_t playstepmicros = micros();
+ // grids_.clockTick(playstepmicros, clockConfig.step_micros);
+
+ if (midiModeception)
+ {
+ midiKeyboard.loopUpdate(elapsedTime);
+ return;
+ }
+
+ auto keyState = midiSettings.keyState;
+
+ f1_ = keyState[1] && !keyState[2];
+ f2_ = !keyState[1] && keyState[2];
+ f3_ = keyState[1] && keyState[2];
+ fNone_ = !keyState[1] && !keyState[2];
+}
+
+void OmxModeGrids::setPageAndParam(uint8_t pageIndex, uint8_t paramPosition)
+{
+ encoderSelect = false;
+ params.setSelPage(pageIndex);
+ // int p = pageIndex * NUM_DISP_PARAMS + paramPosition;
+ setParam(paramPosition);
+ omxDisp.setDirty();
+}
+
+void OmxModeGrids::setParam(uint8_t paramIndex)
+{
+ params.setSelParam(paramIndex);
+
+ // if (paramIndex >= 0)
+ // {
+ // param = paramIndex % kNumParams;
+ // }
+ // else
+ // {
+ // param = (paramIndex + kNumParams) % kNumParams;
+ // }
+ // page = param / NUM_DISP_PARAMS;
+
+ // Select instrument on this page
+ if (instLockView_ && params.getSelPage() == GRIDS_DENSITY)
+ {
+ lockedInst_ = paramIndex;
+
+ // int pIndex = param % NUM_DISP_PARAMS;
+ // if(pIndex > 0){
+ // lockedInst_ = pIndex - 1;
+ // }
+ }
+ omxDisp.setDirty();
+}
+
+// Handles selecting params using encoder
+void OmxModeGrids::onEncoderChangedSelectParam(Encoder::Update enc)
+{
+ if (enc.dir() == 0)
+ return;
+
+ if (enc.dir() < 0) // if turn CCW
+ {
+ params.decrementParam();
+ }
+ else if (enc.dir() > 0) // if turn CW
+ {
+ params.incrementParam();
+ }
+
+ omxDisp.setDirty();
+}
+
+void OmxModeGrids::onEncoderChanged(Encoder::Update enc)
+{
+ if (midiModeception)
+ {
+ midiKeyboard.onEncoderChanged(enc);
+ return;
+ }
+
+ if (encoderSelect)
+ {
+ onEncoderChangedSelectParam(enc);
+ return;
+ }
+
+ if (f1_)
+ {
+ // // Change selected param while holding F1
+ // if (enc.dir() < 0) // if turn CCW
+ // {
+ // setParam(param - 1);
+ // omxDisp.setDirty();
+ // }
+ // else if (enc.dir() > 0) // if turn CW
+ // {
+ // setParam(param + 1);
+ // omxDisp.setDirty();
+ // }
+
+ return; // break;
+ }
+
+ auto amt = enc.accel(5); // where 5 is the acceleration factor if you want it, 0 if you don't)
+
+ // int paramStep = param % 5;
+
+ int8_t selPage = params.getSelPage();
+ int8_t selParam = params.getSelParam() + 1; // Add one for readability
+
+ // if (paramStep != 0) // Page select mode if 0
+ // {
+ // }
+
+ switch (selPage)
+ {
+ case GRIDS_DENSITY:
+ {
+ int newDensity = constrain(grids_.getDensity(selParam - 1) + amt, 0, 255);
+ grids_.setDensity(selParam - 1, newDensity);
+ }
+ break;
+ case GRIDS_XY:
+ {
+ if (selParam == 1) // Accent
+ {
+ int newAccent = constrain(grids_.accent + amt, 0, 255);
+ grids_.accent = newAccent;
+ }
+ else if (selParam == 2) // GridX
+ {
+ if (instLockView_)
+ {
+ int newX = constrain(grids_.getX(lockedInst_) + amt, 0, 255);
+ grids_.setX(lockedInst_, newX);
+ }
+ else
+ {
+ bool gridSel = false;
+
+ for (int g = 0; g < kNumGrids; g++)
+ {
+ if (gridsSelected[g])
+ {
+ int newX = constrain(grids_.getX(g) + amt, 0, 255);
+ grids_.setX(g, newX);
+ gridSel = true;
+ }
+ }
+ if (!gridSel) // No grids selected, modify 0
+ {
+ int newX = constrain(grids_.getX(0) + amt, 0, 255);
+ grids_.setX(0, newX);
+
+ // for (int g = 0; g < kNumGrids; g++)
+ // {
+ // int newX = constrain(grids_.getX(g) + amt, 0, 255);
+ // // gridsXY[g][0] = newX;
+ // grids_.setX(g, newX);
+ // }
+ }
+ }
+ }
+ else if (selParam == 3) // GridY
+ {
+ if (instLockView_)
+ {
+ int newY = constrain(grids_.getY(lockedInst_) + amt, 0, 255);
+ grids_.setY(lockedInst_, newY);
+ }
+ else
+ {
+ bool gridSel = false;
+ for (int g = 0; g < kNumGrids; g++)
+ {
+ if (gridsSelected[g])
+ {
+ int newY = constrain(grids_.getY(g) + amt, 0, 255);
+ grids_.setY(g, newY);
+ gridSel = true;
+ }
+ }
+ if (!gridSel) // No grids selected, modify 0
+ {
+ int newY = constrain(grids_.getY(0) + amt, 0, 255);
+ grids_.setY(0, newY);
+
+ // for (int g = 0; g < kNumGrids; g++)
+ // {
+ // int newY = constrain(grids_.getY(g) + amt, 0, 255);
+ // grids_.setY(g, newY);
+ // }
+ }
+ }
+ }
+ else if (selParam == 4) // Chaos
+ {
+ int newChaos = constrain(grids_.chaos + amt, 0, 255);
+ grids_.chaos = newChaos;
+ }
+ }
+ break;
+ case GRIDS_NOTES:
+ {
+ if (selParam == 1)
+ {
+ grids_.grids_notes[0] = constrain(grids_.grids_notes[0] + amt, 0, 127);
+ }
+ else if (selParam == 2)
+ {
+ grids_.grids_notes[1] = constrain(grids_.grids_notes[1] + amt, 0, 127);
+ }
+ else if (selParam == 3)
+ {
+ grids_.grids_notes[2] = constrain(grids_.grids_notes[2] + amt, 0, 127);
+ }
+ else if (selParam == 4)
+ {
+ grids_.grids_notes[3] = constrain(grids_.grids_notes[3] + amt, 0, 127);
+ }
+ }
+ break;
+ case GRIDS_CONFIG:
+ {
+ if (instLockView_)
+ {
+ if (selParam == 1) // Note
+ {
+ grids_.grids_notes[lockedInst_] = constrain(grids_.grids_notes[lockedInst_] + amt, 0, 127);
+ }
+ else if (selParam == 2) // Note Length
+ {
+ uint8_t noteLength = grids_.getNoteLength(lockedInst_);
+ uint8_t newNoteLength = constrain(noteLength + amt, 0, kNumNoteLengths - 1);
+
+ if (noteLength != newNoteLength)
+ {
+ grids_.setNoteLength(lockedInst_, newNoteLength);
+ omxDisp.displayMessage(kNoteLengths[newNoteLength]);
+ omxDisp.setDirty();
+ }
+ }
+ else if (selParam == 3) // Midi Channel
+ {
+ auto chan = grids_.getMidiChan(lockedInst_);
+ chan = constrain(chan + amt, 1, 16);
+ grids_.setMidiChan(lockedInst_, chan);
+ }
+ }
+
+ if (selParam == 4) // Tempo
+ {
+ clockConfig.newtempo = constrain(clockConfig.clockbpm + amt, 40, 300);
+ if (clockConfig.newtempo != clockConfig.clockbpm)
+ {
+ // SET TEMPO HERE
+ clockConfig.clockbpm = clockConfig.newtempo;
+ omxUtil.resetClocks();
+ }
+ }
+ }
+ break;
+ case GRIDS_CONFIG2:
+ {
+ if (selParam == 1) // Tempo
+ {
+ uint8_t swing = grids_.getSwing();
+ uint8_t newSwing = constrain(swing + amt, 0, 99);
+ grids_.setSwing(newSwing);
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ omxDisp.setDirty();
+}
+
+void OmxModeGrids::onEncoderButtonDown()
+{
+ if (midiModeception)
+ {
+ midiKeyboard.onEncoderButtonDown();
+ return;
+ }
+
+ encoderSelect = !encoderSelect;
+ omxDisp.isDirty();
+
+ // param = (param + 1 ) % kNumParams;
+ // setParam(param);
+}
+
+void OmxModeGrids::onEncoderButtonDownLong()
+{
+ if (midiModeception)
+ {
+ midiKeyboard.onEncoderButtonDownLong();
+ return;
+ }
+}
+
+bool OmxModeGrids::shouldBlockEncEdit()
+{
+ if (midiModeception)
+ {
+ return midiKeyboard.shouldBlockEncEdit();
+ }
+
+ return false;
+}
+
+void OmxModeGrids::saveActivePattern(uint8_t pattIndex)
+{
+ grids_.saveSnapShot(pattIndex);
+
+ // // F2 + PATTERN TO SAVE
+ // for (int k = 0; k < 4; k++)
+ // {
+ // grids_.gridSaves[pattIndex][k].density = grids_.getDensity(k);
+ // grids_.gridSaves[pattIndex][k].x = grids_.getX(k);
+ // grids_.gridSaves[pattIndex][k].y = grids_.getY(k);
+ // // Serial.print("saved:");
+ // // Serial.print(grids_wrapper.gridSaves[patt][k].density);
+ // // Serial.print(":");
+ // // Serial.print(grids_wrapper.gridSaves[patt][k].x);
+ // // Serial.print(":");
+ // // Serial.println(grids_wrapper.gridSaves[patt][k].y);
+ // }
+
+ String msg = "Saved " + String(pattIndex + 1);
+ omxDisp.displayMessageTimed(msg, 5);
+}
+
+void OmxModeGrids::loadActivePattern(uint8_t pattIndex)
+{
+ grids_.loadSnapShot(pattIndex);
+
+ for (uint8_t i = 0; i < NUM_CC_POTS; i++)
+ {
+ potPostLoadThresh[i] = true;
+ }
+
+ // // SELECT
+ // grids_.playingPattern = pattIndex;
+ // for (int k = 0; k < 4; k++)
+ // {
+ // grids_.setDensity(k, grids_.gridSaves[pattIndex][k].density);
+ // grids_.setX(k, grids_.gridSaves[pattIndex][k].x);
+ // grids_.setY(k, grids_.gridSaves[pattIndex][k].y);
+ // // Serial.print("state:");
+ // // Serial.print(grids_wrapper.gridSaves[patt][k].density);
+ // // Serial.print(":");
+ // // Serial.print(grids_wrapper.gridSaves[patt][k].x);
+ // // Serial.print(":");
+ // // Serial.println(grids_wrapper.gridSaves[patt][k].y);
+ // }
+
+ String msg = "Load " + String(pattIndex + 1);
+ omxDisp.displayMessageTimed(msg, 5);
+}
+
+void OmxModeGrids::startPlayback()
+{
+ gridsAUX = true;
+ omxUtil.resetClocks();
+ grids_.start();
+ omxUtil.startClocks();
+ // sequencer.playing = true;
+ isPlaying_ = true;
+}
+void OmxModeGrids::stopPlayback()
+{
+ gridsAUX = false;
+ grids_.stop();
+ omxUtil.stopClocks();
+ // sequencer.playing = false;
+ pendingNoteOffs.allOff();
+ isPlaying_ = false;
+}
+
+// Called by a grids sequencer when it triggers a note
+void OmxModeGrids::onNoteTriggered(uint8_t gridsChannel, MidiNoteGroup note)
+{
+ // Serial.println("OmxModeEuclidean::onNoteTriggered " + String(euclidIndex) + " note: " + String(note.noteNumber));
+
+ // uint8_t mfxIndex = euclids[euclidIndex].midiFXGroup;
+
+ // subModeMidiFx[mfxIndex].noteInput(note);
+
+ // omxDisp.setDirty();
+
+ if (note.noteOff)
+ {
+ // Serial.println("onNotePostFX note off: " + String(note.noteNumber));
+ pendingNoteOns.remove(note.noteNumber, note.channel);
+ pendingNoteOffs.sendOffNow(note.noteNumber, note.channel, note.sendCV);
+ }
+ else
+ {
+ // Serial.println("onNotePostFX note on: " + String(note.noteNumber));
+ // Serial.println("OmxModeEuclidean::onNotePostFX note: " + String(note.noteNumber));
+
+ // Kill any on notes if they were on
+ pendingNoteOns.remove(note.noteNumber, note.channel);
+ pendingNoteOffs.sendOffNow(note.noteNumber, note.channel, note.sendCV);
+
+ uint32_t noteOnMicros = note.noteonMicros; // TODO Might need to be set to current micros
+ pendingNoteOns.insert(note.noteNumber, note.velocity, note.channel, noteOnMicros, note.sendCV);
+
+ uint32_t noteOffMicros = noteOnMicros + (note.stepLength * clockConfig.step_micros);
+ pendingNoteOffs.insert(note.noteNumber, note.channel, noteOffMicros, note.sendCV);
+ }
+
+ omxLeds.setDirty();
+
+ // Serial.println("\n\n");
+}
+
+void OmxModeGrids::onKeyUpdate(OMXKeypadEvent e)
+{
+ int thisKey = e.key();
+ auto keyState = midiSettings.keyState;
+
+ if (midiModeception)
+ {
+ midiKeyboard.onKeyUpdate(e);
+
+ if (midiSettings.keyState[0] && e.down() && thisKey == 26)
+ {
+ midiModeception = false;
+ midiSettings.midiAUX = false;
+ omxDisp.setDirty();
+ omxLeds.setDirty();
+ }
+
+ return;
+ }
+
+ if (instLockView_)
+ {
+ onKeyUpdateChanLock(e);
+ return;
+ }
+
+ if (!e.held())
+ {
+ if (e.down() && thisKey == 0) // Aux key down
+ {
+ // Sequencer shouldn't be a dependancy here but current is used to advance clocks.
+ if (isPlaying_ && gridsAUX)
+ {
+ stopPlayback();
+ }
+ else
+ {
+ startPlayback();
+ }
+ }
+ else if (e.down() && e.clicks() == 0 && (thisKey > 2 && thisKey < 11))
+ {
+ int patt = thisKey - 3;
+
+ if (f2_)
+ {
+ saveActivePattern(patt);
+ }
+ else if (fNone_)
+ {
+ loadActivePattern(patt);
+ }
+ }
+ }
+
+ if (fNone_)
+ {
+ // Select Grid X param
+ if (e.down() && (thisKey > 10 && thisKey < 15))
+ {
+ gridsSelected[thisKey - 11] = true;
+ setPageAndParam(GRIDS_XY, 1);
+ // setParam(GRIDS_XY, 2);
+ omxDisp.setDirty();
+ }
+ else if (!e.down() && (thisKey > 10 && thisKey < 15))
+ {
+ gridsSelected[thisKey - 11] = false;
+ omxDisp.setDirty();
+ }
+
+ // Select Grid Y param
+ if (e.down() && (thisKey > 14 && thisKey < 19))
+ {
+ gridsSelected[thisKey - 15] = true;
+ setPageAndParam(GRIDS_XY, 2);
+ // setParam(GRIDS_XY, 3);
+ omxDisp.setDirty();
+ }
+ else if (!e.down() && (thisKey > 14 && thisKey < 19))
+ {
+ gridsSelected[thisKey - 15] = false;
+ omxDisp.setDirty();
+ }
+
+ // Select Grid X param
+ if (e.down() && thisKey == 23) // Accent
+ {
+ setPageAndParam(GRIDS_XY, 0);
+ // setParam(GRIDS_XY, 1);
+ }
+ else if (e.down() && thisKey == 24) // Xaos
+ {
+ setPageAndParam(GRIDS_XY, 3);
+ // setParam(GRIDS_XY, 4);
+ }
+ else if (e.down() && thisKey == 26) // BPM
+ {
+ setPageAndParam(GRIDS_CONFIG, 3);
+ // setParam(GRIDS_CONFIG, 4);
+ }
+ }
+ if (f1_)
+ {
+ // Quick Select Note
+ if (e.down() && (thisKey > 10 && thisKey < 15))
+ {
+ quickSelectInst(thisKey - 11);
+ }
+
+ if (e.down() && thisKey == 26)
+ {
+ midiKeyboard.onModeActivated();
+ midiModeception = true;
+ omxDisp.setDirty();
+ omxLeds.setDirty();
+ }
+
+ // else if (!e.down() && (thisKey > 10 && thisKey < 15))
+ // {
+ // }
+
+ // Select Grid Y param
+ // if (e.down() && (thisKey > 14 && thisKey < 19))
+ // {
+ // setParam(GRIDS_NOTES * NUM_DISP_PARAMS + (thisKey - 14));
+ // omxDisp.setDirty();
+ // }
+ // else if (!e.down() && (thisKey > 14 && thisKey < 19))
+ // {
+
+ // }
+ }
+ // RANDOM X or Y with bottom key + F2
+ for (int j = 11; j < 19; j++)
+ {
+ if (keyState[j])
+ {
+ if (e.down() && (thisKey == 2))
+ {
+ if (j < 15)
+ {
+ int whichX = j - 11;
+ int newX = random(0, 255);
+ grids_.setX(whichX, newX);
+ setPageAndParam(GRIDS_XY, 1);
+ // setParam(GRIDS_XY, 2);
+ }
+ else
+ {
+ int whichY = j - 15;
+ int newY = random(0, 255);
+ grids_.setY(whichY, newY);
+ setPageAndParam(GRIDS_XY, 2);
+ // setParam(GRIDS_XY, 3);
+ }
+ omxDisp.setDirty();
+ }
+ }
+ }
+}
+
+void OmxModeGrids::onKeyUpdateChanLock(OMXKeypadEvent e)
+{
+ int thisKey = e.key();
+
+ auto keyState = midiSettings.keyState;
+
+ if (!e.held())
+ {
+ if (e.down() && thisKey == 0) // Aux key down
+ {
+ // Serial.println("Exit aux");
+ instLockView_ = false; // Exit out of channel lock
+ omxDisp.setDirty();
+ return;
+ }
+ // else if (e.down() && e.clicks() == 0 && (thisKey > 2 && thisKey < 11))
+ // {
+ // int patt = thisKey - 3;
+
+ // if (f2_)
+ // {
+ // saveActivePattern(patt);
+ // }
+ // else if(fNone_)
+ // {
+ // loadActivePattern(patt);
+ // }
+ // }
+ }
+
+ // Function quick keys
+ if (!f2_ && e.down() && thisKey == 2 && !keyState[1])
+ {
+ setPageAndParam(GRIDS_CONFIG, 0);
+ // setParam(GRIDS_CONFIG, 1);
+ }
+ // if (!f3_ && e.down() && ((thisKey == 1 && keyState[2]) || (thisKey == 2 && keyState[1])))
+ // {
+ // setParam(GRIDS_CONFIG, 2);
+ // }
+
+ // if (f2_)
+ // {
+ // setParam(GRIDS_CONFIG, 1);
+ // }
+ // else if (f3_)
+ // {
+ // setParam(GRIDS_CONFIG, 2);
+ // }
+
+ if (!f1_)
+ {
+ justLocked_ = false; // False once F1 released
+ }
+
+ if (fNone_)
+ {
+ if (e.down() && thisKey == 3) // Note Number
+ {
+ setPageAndParam(GRIDS_CONFIG, 0);
+ // setParam(GRIDS_CONFIG, 2);
+ }
+ if (e.down() && thisKey == 4) // Note Length
+ {
+ setPageAndParam(GRIDS_CONFIG, 1);
+ // setParam(GRIDS_CONFIG, 2);
+ }
+ // Select Grid X param
+ if (e.down() && thisKey == 5) // Accent
+ {
+ setPageAndParam(GRIDS_XY, 0);
+ // setParam(GRIDS_XY, 1);
+ }
+ if (e.down() && thisKey == 6) // Chan X
+ {
+ setPageAndParam(GRIDS_XY, 1);
+ // setParam(GRIDS_XY, 2);
+ }
+ if (e.down() && thisKey == 7) // Chan Y
+ {
+ setPageAndParam(GRIDS_XY, 2);
+ // setParam(GRIDS_XY, 3);
+ }
+ if (e.down() && thisKey == 8) // Xaos
+ {
+ setPageAndParam(GRIDS_XY, 3);
+ // setParam(GRIDS_XY, 4);
+ }
+ if (e.down() && thisKey == 9) // Midi Chan
+ {
+ setPageAndParam(GRIDS_CONFIG, 2);
+ // setParam(GRIDS_XY, 4);
+ }
+ if (e.down() && thisKey == 10) // BPM
+ {
+ setPageAndParam(GRIDS_CONFIG, 3);
+ // setParam(GRIDS_XY, 4);
+ }
+
+ // Quick Select inst
+ // These LEDs are not lit since pattern is rendered
+ if (e.down() && (thisKey > 10 && thisKey < 15))
+ {
+ quickSelectInst(thisKey - 11);
+ }
+ else if (e.down() && thisKey == 26) // BPM
+ {
+ setPageAndParam(GRIDS_CONFIG, 3);
+ // setParam(GRIDS_CONFIG, 4);
+ }
+ }
+ if (f1_ && !justLocked_)
+ {
+ // Quick Select Inst
+ if (e.down() && (thisKey > 10 && thisKey < 15))
+ {
+ quickSelectInst(thisKey - 11);
+ }
+
+ if (e.down() && thisKey == 26)
+ {
+ midiKeyboard.onModeActivated();
+ midiModeception = true;
+ omxDisp.setDirty();
+ omxLeds.setDirty();
+ }
+ }
+}
+
+void OmxModeGrids::quickSelectInst(uint8_t instIndex)
+{
+ if (instLockView_ && lockedInst_ == instIndex)
+ return;
+
+ instLockView_ = true;
+ // justLocked_ = true; // Uncomment to immediately switch to channel view
+ lockedInst_ = instIndex;
+
+ if (params.getSelPage() == GRIDS_DENSITY || params.getSelPage() == GRIDS_NOTES)
+ {
+ setParam(lockedInst_);
+ // setParam(page, lockedInst_ + 1);
+ }
+
+ String msg = "Inst " + String(lockedInst_ + 1);
+ omxDisp.displayMessageTimed(msg, 5);
+ omxDisp.setDirty();
+}
+
+void OmxModeGrids::onKeyHeldUpdate(OMXKeypadEvent e)
+{
+ if (midiModeception)
+ {
+ midiKeyboard.onKeyHeldUpdate(e);
+ return;
+ }
+}
+
+void OmxModeGrids::updateLEDs()
+{
+ if (midiModeception)
+ {
+ return;
+ }
+
+ // omxLeds.updateBlinkStates();
+
+ bool blinkState = omxLeds.getBlinkState();
+
+ if (instLockView_)
+ {
+ int64_t instLockColor = paramSelColors[lockedInst_];
+
+ // Always blink to show you're in mode, don't need differation between playing or not since the playhead makes this obvious
+ // auto color1 = blinkState ? instLockColor : LEDOFF;
+ // strip.setPixelColor(0, color1);
+
+ if (isPlaying_)
+ {
+ // Blink left/right keys for octave select indicators.
+ auto color1 = blinkState ? instLockColor : LEDOFF;
+ strip.setPixelColor(0, color1);
+ }
+ else
+ {
+ strip.setPixelColor(0, instLockColor);
+ }
+ }
+ else
+ {
+ if (isPlaying_)
+ {
+ // Blink left/right keys for octave select indicators.
+ auto color1 = blinkState ? LIME : LEDOFF;
+ strip.setPixelColor(0, color1);
+ }
+ else
+ {
+ strip.setPixelColor(0, LEDOFF);
+ }
+ }
+
+ // Function Keys
+ if (f3_)
+ {
+ auto f3Color = blinkState ? LEDOFF : FUNKTHREE;
+ strip.setPixelColor(1, f3Color);
+ strip.setPixelColor(2, f3Color);
+ }
+ else
+ {
+ auto f1Color = (f1_ && blinkState) ? LEDOFF : FUNKONE;
+ strip.setPixelColor(1, f1Color);
+
+ auto f2Color = (f2_ && blinkState) ? LEDOFF : FUNKTWO;
+ strip.setPixelColor(2, f2Color);
+ }
+
+ if (instLockView_)
+ {
+ updateLEDsChannelView();
+ }
+ else
+ {
+ updateLEDsPatterns();
+
+ // Set 16 key leds to off to prevent them from sticking on after screensaver.
+ for (int k = 0; k < 16; k++)
+ {
+ strip.setPixelColor(k + 11, LEDOFF);
+ }
+
+ if (fNone_ || f2_)
+ updateLEDsFNone();
+ else if (f1_)
+ updateLEDsF1();
+ }
+
+ omxLeds.setDirty();
+}
+
+void OmxModeGrids::updateLEDsFNone()
+{
+ bool blinkState = omxLeds.getBlinkState();
+
+ // uint32_t colors[8] = {};
+ // colors[0] = blinkState ? MAGENTA : LEDOFF;
+ // colors[1] = blinkState ? ORANGE : LEDOFF;
+ // colors[2] = blinkState ? RED : LEDOFF;
+ // colors[3] = blinkState ? RBLUE : LEDOFF;
+ // colors[4] = blinkState ? MAGENTA : LEDOFF;
+ // colors[5] = blinkState ? ORANGE : LEDOFF;
+ // colors[6] = blinkState ? RED : LEDOFF;
+ // colors[7] = blinkState ? RBLUE : LEDOFF;
+
+ auto keyState = midiSettings.keyState;
+
+ for (int k = 0; k < 4; k++)
+ {
+ // Change color of 4 GridX keys when pushed
+ // auto kColor = keyState[k + 11] ? (blinkState ? paramSelColors[k] : LEDOFF) : PINK;
+ auto kColor = keyState[k + 11] ? (blinkState ? paramSelColors[k] : LEDOFF) : BLUE;
+
+ strip.setPixelColor(k + 11, kColor);
+ }
+
+ for (int k = 4; k < 8; k++)
+ {
+ // Change color of 4 GridY keys when pushed
+ // auto kColor = keyState[k + 11] ? (blinkState ? paramSelColors[k % 4] : LEDOFF) : GREEN;
+ auto kColor = keyState[k + 11] ? (blinkState ? paramSelColors[k % 4] : LEDOFF) : LTCYAN;
+ strip.setPixelColor(k + 11, kColor);
+ }
+
+ for (int k = 0; k < 4; k++)
+ {
+ bool triggered = grids_.getChannelTriggered(k);
+ // Change color of 4 GridY keys when pushed
+ auto kColor = triggered ? paramSelColors[k] : LEDOFF;
+ strip.setPixelColor(k + 19, kColor);
+ }
+
+ strip.setPixelColor(23, (keyState[23] ? LBLUE : BLUE)); // Accent
+ strip.setPixelColor(24, (keyState[24] ? WHITE : ORANGE)); // Xaos
+ strip.setPixelColor(26, (keyState[26] ? WHITE : MAGENTA)); // BPM
+}
+
+void OmxModeGrids::updateLEDsF1()
+{
+ bool blinkState = omxLeds.getBlinkState();
+ auto keyState = midiSettings.keyState;
+
+ // updateLEDsChannelView();
+
+ for (int k = 0; k < 4; k++)
+ {
+ // Change color of 4 GridX keys when pushed
+ auto kColor = keyState[k + 11] ? (blinkState ? paramSelColors[k] : LEDOFF) : ORANGE;
+ strip.setPixelColor(k + 11, kColor);
+ }
+
+ for (int k = 4; k < 16; k++)
+ {
+ strip.setPixelColor(k + 11, LEDOFF);
+ }
+
+ strip.setPixelColor(26, ORANGE);
+}
+
+void OmxModeGrids::updateLEDsChannelView()
+{
+ // bool blinkState = omxLeds.getBlinkState();
+ auto keyState = midiSettings.keyState;
+
+ int seqPos = 0;
+
+ if (isPlaying_)
+ {
+ seqPos = grids_.getSeqPos();
+ }
+
+ if (f1_ && !justLocked_)
+ {
+ updateLEDsF1();
+ for (int j = 3; j < LED_COUNT - 16; j++)
+ {
+ strip.setPixelColor(j, LEDOFF);
+ }
+ }
+ else
+ {
+ // Shortcut LEDS for top row
+ for (int j = 3; j < LED_COUNT - 16; j++)
+ {
+ if (j == 3) // Note Number
+ {
+ strip.setPixelColor(j, (keyState[3] ? LBLUE : DKBLUE));
+ }
+ else if (j == 4) // Note Length
+ {
+ strip.setPixelColor(j, (keyState[4] ? LBLUE : DKBLUE));
+ }
+ else if (j == 5) // Accent
+ {
+ strip.setPixelColor(j, (keyState[5] ? WHITE : BLUE));
+ }
+ else if (j == 6) // ChanX
+ {
+ strip.setPixelColor(j, (keyState[6] ? WHITE : RED));
+ }
+ else if (j == 7) // Chan Y
+ {
+ strip.setPixelColor(j, (keyState[7] ? WHITE : GREEN));
+ }
+ else if (j == 8) // Chaos
+ {
+ strip.setPixelColor(j, (keyState[8] ? WHITE : ORANGE));
+ }
+ else if (j == 9) // Midi Chan
+ {
+ strip.setPixelColor(j, (keyState[9] ? WHITE : RED));
+ }
+ else if (j == 10) // BPM
+ {
+ strip.setPixelColor(j, (keyState[9] ? WHITE : RED));
+ }
+ // else if (j == 10) // Tempo
+ // {
+ // strip.setPixelColor(j, (keyState[8] ? WHITE : MAGENTA));
+ // }
+ else
+ {
+ strip.setPixelColor(j, LEDOFF);
+ }
+ }
+
+ auto channelLeds = grids_.getChannelLEDS(lockedInst_);
+
+ auto channelHue = instLockHues_[lockedInst_];
+
+ auto seqStart = seqPos >= 16 ? 16 : 0;
+
+ for (int k = 0; k < 16; k++)
+ {
+ // Change color of 4 GridX keys when pushed
+ auto level = channelLeds.levels[seqStart + k] * 2;
+ auto kColor = strip.ColorHSV(channelHue, 255, level);
+ strip.setPixelColor(k + 11, kColor);
+ }
+
+ if (isPlaying_)
+ {
+ auto seq16 = seqPos % 16;
+ strip.setPixelColor(seq16 + 11, HALFWHITE);
+ }
+ }
+}
+
+void OmxModeGrids::updateLEDsPatterns()
+{
+ int patternNum = grids_.playingPattern;
+
+ // LEDS for top row
+ for (int j = 3; j < LED_COUNT - 16; j++)
+ {
+ auto pColor = (j == patternNum + 3) ? seqColors[patternNum] : LEDOFF;
+ strip.setPixelColor(j, pColor);
+ }
+}
+
+void OmxModeGrids::setupPageLegends()
+{
+ // if (midiSettings.keyState[11] || midiSettings.keyState[15])
+ // {
+ // thisGrid = 0;
+ // }
+ // else if (keyState[12] || keyState[16])
+ // {
+ // thisGrid = 1;
+ // }
+ // else if (keyState[13] || keyState[17])
+ // {
+ // thisGrid = 2;
+ // }
+ // else if (keyState[14] || keyState[18])
+ // {
+ // thisGrid = 3;
+ // }
+
+ omxDisp.clearLegends();
+
+ // omxDisp.dispPage = page + 1;
+
+ int8_t page = params.getSelPage();
+
+ switch (page)
+ {
+ case GRIDS_DENSITY:
+ {
+ omxDisp.legends[0] = "DS 1";
+ omxDisp.legends[1] = "DS 2";
+ omxDisp.legends[2] = "DS 3";
+ omxDisp.legends[3] = "DS 4";
+ omxDisp.legendVals[0] = grids_.getDensity(0);
+ omxDisp.legendVals[1] = grids_.getDensity(1);
+ omxDisp.legendVals[2] = grids_.getDensity(2);
+ omxDisp.legendVals[3] = grids_.getDensity(3);
+ }
+ break;
+ case GRIDS_XY:
+ {
+ int targetChannel = 0;
+ bool setLegendsToChannel = false;
+
+ if (instLockView_)
+ {
+ targetChannel = lockedInst_;
+ setLegendsToChannel = true;
+ }
+ else
+ {
+ int numGrids = sizeof(gridsSelected);
+ int selGridsCount = 0;
+
+ // Calculate which channels are selected
+ for (int i = 0; i < numGrids; i++)
+ {
+ if (gridsSelected[i])
+ {
+ selGridsCount++;
+ targetChannel = i;
+ }
+ }
+
+ if (selGridsCount == 0)
+ {
+ targetChannel = 0;
+ setLegendsToChannel = true;
+ }
+ else if (selGridsCount == 1)
+ {
+ setLegendsToChannel = true;
+ }
+ else if (selGridsCount == 4)
+ {
+ omxDisp.legends[1] = "X All";
+ omxDisp.legends[2] = "Y All";
+ }
+ else
+ {
+ omxDisp.legends[1] = "X *";
+ omxDisp.legends[2] = "Y *";
+ }
+ }
+
+ if (setLegendsToChannel)
+ {
+ // Not sure why string.c_str doesn't work
+ xTemp = "X " + String(targetChannel + 1);
+ yTemp = "Y " + String(targetChannel + 1);
+
+ omxDisp.legends[1] = xTemp.c_str();
+ omxDisp.legends[2] = yTemp.c_str();
+
+ // char bufx[4];
+ // char bufy[4];
+ // snprintf(bufx, sizeof(bufx), "X %d", thisGrid + 1);
+ // snprintf(bufy, sizeof(bufy), "Y %d", thisGrid + 1);
+
+ // omxDisp.legends[1] = bufx;
+ // omxDisp.legends[2] = bufy;
+
+ // Above string code not working at all? This is ugly
+ // if (targetChannel == 0)
+ // {
+ // omxDisp.legends[1] = "X 1";
+ // omxDisp.legends[2] = "Y 1";
+ // }
+ // else if (targetChannel == 1)
+ // {
+ // omxDisp.legends[1] = "X 2";
+ // omxDisp.legends[2] = "Y 2";
+ // }
+ // else if (targetChannel == 2)
+ // {
+ // omxDisp.legends[1] = "X 3";
+ // omxDisp.legends[2] = "Y 3";
+ // }
+ // else if (targetChannel == 3)
+ // {
+ // omxDisp.legends[1] = "X 4";
+ // omxDisp.legends[2] = "Y 4";
+ // }
+ }
+
+ omxDisp.legends[0] = "ACNT"; // "BPM";
+ omxDisp.legends[3] = "XAOS";
+ omxDisp.legendVals[0] = grids_.accent; // (int)clockbpm;
+ if (targetChannel != -1)
+ {
+ omxDisp.legendVals[1] = grids_.getX(targetChannel);
+ omxDisp.legendVals[2] = grids_.getY(targetChannel);
+ }
+ omxDisp.legendVals[3] = grids_.chaos;
+ }
+ break;
+ case GRIDS_NOTES:
+ {
+ omxDisp.legends[0] = "NT 1";
+ omxDisp.legends[1] = "NT 2";
+ omxDisp.legends[2] = "NT 3";
+ omxDisp.legends[3] = "NT 4";
+ omxDisp.legendVals[0] = grids_.grids_notes[0];
+ omxDisp.legendVals[1] = grids_.grids_notes[1];
+ omxDisp.legendVals[2] = grids_.grids_notes[2];
+ omxDisp.legendVals[3] = grids_.grids_notes[3];
+ }
+ break;
+ case GRIDS_CONFIG:
+ {
+ if (instLockView_)
+ {
+ legendTemp = "NT " + String(lockedInst_ + 1);
+
+ omxDisp.legends[0] = legendTemp.c_str();
+ omxDisp.legends[1] = "GATE";
+ omxDisp.legends[2] = "M-CHAN";
+ omxDisp.legends[3] = "BPM";
+ omxDisp.legendVals[0] = grids_.grids_notes[lockedInst_];
+ omxDisp.legendVals[1] = grids_.getNoteLength(lockedInst_);
+ omxDisp.legendVals[2] = grids_.getMidiChan(lockedInst_);
+ omxDisp.legendVals[3] = (int)clockConfig.clockbpm;
+ }
+ else
+ {
+ omxDisp.legends[0] = "";
+ omxDisp.legends[1] = "";
+ omxDisp.legends[2] = "";
+ omxDisp.legends[3] = "BPM";
+ omxDisp.legendVals[0] = -127;
+ omxDisp.legendVals[1] = -127;
+ omxDisp.legendVals[2] = -127;
+ omxDisp.legendVals[3] = (int)clockConfig.clockbpm;
+ omxDisp.legendText[0] = "";
+ omxDisp.legendText[1] = "";
+ omxDisp.legendText[2] = "";
+ }
+ }
+ break;
+ case GRIDS_CONFIG2:
+ {
+ omxDisp.legends[0] = "SWNG";
+ omxDisp.legendVals[0] = grids_.getSwing();
+ }
+ break;
+ default:
+ break;
+ }
+}
+
+void OmxModeGrids::onDisplayUpdate()
+{
+ if (midiModeception)
+ {
+ midiKeyboard.onDisplayUpdate();
+
+ if (midiSettings.midiAUX)
+ {
+ strip.setPixelColor(26, RED); // Highlight aux exit key
+ }
+
+ return;
+ }
+
+ updateLEDs();
+
+ if (omxDisp.isDirty())
+ { // DISPLAY
+ // Serial.println("Disp dirty");
+
+ if (!encoderConfig.enc_edit)
+ {
+ setupPageLegends();
+ omxDisp.dispGenericMode2(params.getNumPages(), params.getSelPage(), params.getSelParam(), encoderSelect);
+
+ // int pselected = param % NUM_DISP_PARAMS;
+ // omxDisp.dispGenericMode(pselected);
+ }
+ }
+}
+
+void OmxModeGrids::SetScale(MusicScales *scale)
+{
+ midiKeyboard.SetScale(scale);
+}
+
+int OmxModeGrids::serializedPatternSize(bool eeprom)
+{
+ return sizeof(grids::SnapShotSettings);
+}
+
+grids::SnapShotSettings *OmxModeGrids::getPattern(uint8_t patternIndex)
+{
+ return grids_.getSnapShot(patternIndex);
+}
+
+void OmxModeGrids::setPattern(uint8_t patternIndex, grids::SnapShotSettings snapShot)
+{
+ grids_.setSnapShot(patternIndex, snapShot);
+}
diff --git a/Archive/OMX-27-firmware/src/modes/omx_mode_grids.h b/Archive/OMX-27-firmware/src/modes/omx_mode_grids.h
new file mode 100644
index 00000000..bc74f0e2
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/modes/omx_mode_grids.h
@@ -0,0 +1,129 @@
+#pragma once
+
+#include "omx_mode_interface.h"
+#include "../utils/music_scales.h"
+#include "retro_grids.h"
+#include "../consts/colors.h"
+#include "../config.h"
+#include "omx_mode_midi_keyboard.h"
+#include "../utils/param_manager.h"
+
+class OmxModeGrids : public OmxModeInterface
+{
+public:
+ OmxModeGrids();
+ ~OmxModeGrids() {}
+
+ void InitSetup() override;
+
+ void onModeActivated() override;
+ void onModeDeactivated() override;
+
+ void onClockTick() override;
+
+ void onPotChanged(int potIndex, int prevValue, int newValue, int analogDelta) override;
+
+ void loopUpdate(Micros elapsedTime) override;
+
+ void updateLEDs() override;
+
+ void onEncoderChanged(Encoder::Update enc) override;
+ void onEncoderButtonDown() override;
+ void onEncoderButtonDownLong() override;
+
+ bool shouldBlockEncEdit() override;
+
+ void onKeyUpdate(OMXKeypadEvent e) override;
+ void onKeyHeldUpdate(OMXKeypadEvent e) override;
+
+ void onDisplayUpdate() override;
+ void SetScale(MusicScales *scale);
+
+ static int serializedPatternSize(bool eeprom);
+ static inline int getNumPatterns() { return 8; }
+ grids::SnapShotSettings *getPattern(uint8_t patternIndex);
+ void setPattern(uint8_t patternIndex, grids::SnapShotSettings snapShot);
+
+private:
+ void setPageAndParam(uint8_t pageIndex, uint8_t paramPosition);
+ void setParam(uint8_t paramIndex);
+ void setupPageLegends();
+
+ void updateLEDsFNone();
+ void updateLEDsF1();
+ void updateLEDsPatterns();
+
+ void updateLEDsChannelView();
+ void onKeyUpdateChanLock(OMXKeypadEvent e);
+
+ void saveActivePattern(uint8_t pattIndex);
+ void loadActivePattern(uint8_t pattIndex);
+
+ void quickSelectInst(uint8_t instIndex);
+
+ void startPlayback();
+ void stopPlayback();
+
+ bool initSetup = false;
+ grids::GridsWrapper grids_;
+
+ // Static glue to link a pointer to a member function
+ static void onNoteTriggeredForwarder(void *context, uint8_t gridsChannel, MidiNoteGroup note)
+ {
+ static_cast(context)->onNoteTriggered(gridsChannel, note);
+ }
+
+ void onNoteTriggered(uint8_t gridsChannel, MidiNoteGroup note);
+
+ // static const uint8_t kNumPages = 4;
+ // static const uint8_t kNumParams = kNumPages * NUM_DISP_PARAMS;
+ static const uint8_t kNumGrids = 4;
+
+ // static const int kParamGridX = 2;
+ // static const int kParamGridY = 3;
+
+ uint32_t paramSelColors[4] = {MAGENTA, ORANGE, RED, RBLUE};
+
+ const char *rateNames[3] = {"0.5x", "1x", "2x"};
+
+ // If true, encoder selects param rather than modifies value
+ bool encoderSelect = false;
+ void onEncoderChangedSelectParam(Encoder::Update enc);
+ ParamManager params;
+
+ // int page = 0;
+ // int param = 0;
+
+ // int gridXKeyChannel = 0; // Gets set by holding first 0-3 keys on bottom
+ // int gridYKeyChannel = 0; // Gets set by holding keys 4-7 on bottom
+
+ // int gridsXY[4][2];
+
+ bool gridsSelected[4] = {false, false, false, false};
+
+ // Implements threshold post load to prevent pots from changing until modified
+ bool potPostLoadThresh[5] = {false, false, false, false, false};
+
+ bool isPlaying_ = false;
+
+ bool gridsAUX = false;
+
+ bool f1_;
+ bool f2_;
+ bool f3_;
+ bool fNone_;
+
+ bool instLockView_ = false;
+ bool justLocked_ = false;
+ int lockedInst_ = 0;
+ uint16_t instLockHues_[4] = {300, 30, 0, 210};
+
+ int prevResolution_ = 0;
+
+ bool midiModeception = false;
+ OmxModeMidiKeyboard midiKeyboard; // Mode inside a mode. For science!
+
+ String legendTemp;
+ String xTemp;
+ String yTemp;
+};
diff --git a/Archive/OMX-27-firmware/src/modes/omx_mode_interface.h b/Archive/OMX-27-firmware/src/modes/omx_mode_interface.h
new file mode 100644
index 00000000..748a9d21
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/modes/omx_mode_interface.h
@@ -0,0 +1,38 @@
+#pragma once
+#include "../ClearUI/ClearUI_Input.h"
+#include "../hardware/omx_keypad.h"
+#include "../config.h"
+class OmxModeInterface
+{
+public:
+ OmxModeInterface() {}
+ virtual ~OmxModeInterface() {}
+
+ virtual void InitSetup() {} // Called once when mode is created
+
+ virtual void onModeActivated() {} // Called whenever entering mode
+ virtual void onModeDeactivated() {} // Called whenever entering mode
+
+ virtual void onClockTick() {}
+
+ virtual void onPotChanged(int potIndex, int prevValue, int newValue, int analogDelta) = 0;
+ virtual void loopUpdate(Micros elapsedTime) {}
+ virtual void updateLEDs() = 0;
+ virtual void onEncoderChanged(Encoder::Update enc) = 0;
+ virtual void onEncoderButtonDown() = 0;
+ virtual void onEncoderButtonUp(){};
+ virtual void onEncoderButtonUpLong(){};
+
+ virtual bool shouldBlockEncEdit() { return false; } // return true if should block encoder mode switch / hold down encoder
+ virtual void onEncoderButtonDownLong() = 0; // Will only get called if shouldBlockEncEdit() returns true
+
+ virtual void onKeyUpdate(OMXKeypadEvent e) = 0;
+ virtual void onKeyHeldUpdate(OMXKeypadEvent e){};
+
+ virtual void onDisplayUpdate(){};
+
+ // #### Inbound MIDI callbacks
+ virtual void inMidiNoteOn(byte channel, byte note, byte velocity) {}
+ virtual void inMidiNoteOff(byte channel, byte note, byte velocity) {}
+ virtual void inMidiControlChange(byte channel, byte control, byte value) {}
+};
diff --git a/Archive/OMX-27-firmware/src/modes/omx_mode_midi_keyboard.cpp b/Archive/OMX-27-firmware/src/modes/omx_mode_midi_keyboard.cpp
new file mode 100644
index 00000000..b9d82c61
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/modes/omx_mode_midi_keyboard.cpp
@@ -0,0 +1,1513 @@
+#include "omx_mode_midi_keyboard.h"
+#include "../config.h"
+#include "../consts/colors.h"
+#include "../utils/omx_util.h"
+#include "../utils/cvNote_util.h"
+#include "../hardware/omx_disp.h"
+#include "../hardware/omx_leds.h"
+#include "../midi/midi.h"
+#include "../utils/music_scales.h"
+#include "../midi/noteoffs.h"
+#include "sequencer.h"
+
+// const int kSelMidiFXOffColor = SALMON;
+// const int kMidiFXOffColor = RED;
+
+// const int kSelMidiFXColor = LTCYAN;
+// const int kMidiFXColor = BLUE;
+
+enum MIKeyModePage {
+ MIPAGE_OUTMIDI,
+ MIPAGE_MIDIINSPECT,
+ MIPAGE_OUTCC,
+ MIPAGE_POTSANDMACROS,
+ MIPAGE_SCALES,
+ MIPAGE_CFG,
+ MIPAGE_CLOCK_SOURCE,
+ MIPAGE_CLOCK_SEND,
+ MIPAGE_VERSION
+};
+
+OmxModeMidiKeyboard::OmxModeMidiKeyboard()
+{
+ // Add 4 pages
+ params.addPage(4); // Oct, CH, Vel
+ params.addPage(4); // Sent Pot CC, Last Note, Last Vel, Not editable, just FYI
+ params.addPage(4); // RR - Midi Round Robin, RROF - Round Robin Offset, PGm, BNK
+ params.addPage(4); // PotBank, Thru, Macro, Macro Channel
+ params.addPage(4); // Root, Scale, Lock Scale Notes, Group notes.
+ params.addPage(4); // Pot CC CFG
+ params.addPage(4); // MIPAGE_VERSION
+
+ // subModeMidiFx.setNoteOutputFunc(&OmxModeMidiKeyboard::onNotePostFXForwarder, this);
+
+ m8Macro_.setDoNoteOn(&OmxModeMidiKeyboard::doNoteOnForwarder, this);
+ m8Macro_.setDoNoteOff(&OmxModeMidiKeyboard::doNoteOffForwarder, this);
+ nornsMarco_.setDoNoteOn(&OmxModeMidiKeyboard::doNoteOnForwarder, this);
+ nornsMarco_.setDoNoteOff(&OmxModeMidiKeyboard::doNoteOffForwarder, this);
+ delugeMacro_.setDoNoteOn(&OmxModeMidiKeyboard::doNoteOnForwarder, this);
+ delugeMacro_.setDoNoteOff(&OmxModeMidiKeyboard::doNoteOffForwarder, this);
+}
+
+void OmxModeMidiKeyboard::InitSetup()
+{
+ initSetup = true;
+}
+
+void OmxModeMidiKeyboard::onModeActivated()
+{
+ // auto init when activated
+ if (!initSetup)
+ {
+ InitSetup();
+ }
+
+ // sequencer.playing = false;
+ stopSequencers();
+
+ omxLeds.setDirty();
+ omxDisp.setDirty();
+
+ for (uint8_t i = 0; i < NUM_MIDIFX_GROUPS; i++)
+ {
+ subModeMidiFx[i].setEnabled(true);
+ subModeMidiFx[i].onModeChanged();
+ subModeMidiFx[i].setNoteOutputFunc(&OmxModeMidiKeyboard::onNotePostFXForwarder, this);
+ }
+
+ pendingNoteOffs.setNoteOffFunction(&OmxModeMidiKeyboard::onPendingNoteOffForwarder, this);
+
+ params.setSelPageAndParam(0, 0);
+ encoderSelect = true;
+
+ selectMidiFx(mfxIndex_, false);
+}
+
+void OmxModeMidiKeyboard::onModeDeactivated()
+{
+ // sequencer.playing = false;
+ stopSequencers();
+
+ for (uint8_t i = 0; i < NUM_MIDIFX_GROUPS; i++)
+ {
+ subModeMidiFx[i].setEnabled(false);
+ subModeMidiFx[i].onModeChanged();
+ }
+}
+
+void OmxModeMidiKeyboard::stopSequencers()
+{
+ omxUtil.stopClocks();
+ // MM::stopClock();
+ pendingNoteOffs.allOff();
+}
+
+void OmxModeMidiKeyboard::selectMidiFx(uint8_t mfxIndex, bool dispMsg)
+{
+ this->mfxIndex_ = mfxIndex;
+
+ if(mfxQuickEdit_)
+ {
+ // Change the MidiFX Group being edited
+ if(mfxIndex < NUM_MIDIFX_GROUPS && mfxIndex != quickEditMfxIndex_)
+ {
+ enableSubmode(&subModeMidiFx[mfxIndex]);
+ subModeMidiFx[mfxIndex].enablePassthrough();
+ quickEditMfxIndex_ = mfxIndex;
+ dispMsg = false;
+ }
+ else if(mfxIndex >= NUM_MIDIFX_GROUPS)
+ {
+ disableSubmode();
+ }
+ }
+
+ for (uint8_t i = 0; i < NUM_MIDIFX_GROUPS; i++)
+ {
+ subModeMidiFx[i].setSelected(i == mfxIndex);
+ }
+
+ if (dispMsg)
+ {
+ if (mfxIndex < NUM_MIDIFX_GROUPS)
+ {
+ omxDisp.displayMessageTimed("MidiFX " + String(mfxIndex + 1), 5);
+ }
+ else
+ {
+ omxDisp.displayMessageTimed("MidiFX Off", 5);
+ }
+ }
+}
+
+// void OmxModeMidiKeyboard::changePage(int amt)
+// {
+// midiPageParams.mmpage = constrain(midiPageParams.mmpage + amt, 0, midiPageParams.numPages - 1);
+// midiPageParams.miparam = midiPageParams.mmpage * NUM_DISP_PARAMS;
+// }
+
+// void OmxModeMidiKeyboard::setParam(int paramIndex)
+// {
+// if (paramIndex >= 0)
+// {
+// midiPageParams.miparam = paramIndex % midiPageParams.numParams;
+// }
+// else
+// {
+// midiPageParams.miparam = (paramIndex + midiPageParams.numParams) % midiPageParams.numParams;
+// }
+
+// // midiPageParams.miparam = (midiPageParams.miparam + 1) % 15;
+// midiPageParams.mmpage = midiPageParams.miparam / NUM_DISP_PARAMS;
+// }
+
+void OmxModeMidiKeyboard::onPotChanged(int potIndex, int prevValue, int newValue, int analogDelta)
+{
+ if (isSubmodeEnabled() && activeSubmode->usesPots())
+ {
+ activeSubmode->onPotChanged(potIndex, prevValue, newValue, analogDelta);
+ return;
+ }
+
+ auto activeMacro = getActiveMacro();
+
+ bool macroConsumesPots = false;
+ if (activeMacro != nullptr)
+ {
+ macroConsumesPots = activeMacro->consumesPots();
+ }
+
+ // Note, these get sent even if macro mode is not active
+ if (macroConsumesPots)
+ {
+ activeMacro->onPotChanged(potIndex, prevValue, newValue, analogDelta);
+ }
+ else
+ {
+ omxUtil.sendPots(potIndex, sysSettings.midiChannel);
+ }
+
+ // if (midiMacroConfig.midiMacro)
+ // {
+ // omxUtil.sendPots(potIndex, midiMacroConfig.midiMacroChan);
+ // }
+ // else
+ // {
+ // }
+
+ omxDisp.setDirty();
+}
+
+void OmxModeMidiKeyboard::onClockTick()
+{
+ for (uint8_t i = 0; i < NUM_MIDIFX_GROUPS; i++)
+ {
+ // Lets them do things in background
+ subModeMidiFx[i].onClockTick();
+ }
+}
+
+void OmxModeMidiKeyboard::loopUpdate(Micros elapsedTime)
+{
+
+ // if (elapsedTime > 0)
+ // {
+ // if (!sequencer.playing)
+ // {
+ // // Needed to make pendingNoteOns/pendingNoteOffs work
+ // omxUtil.advanceSteps(elapsedTime);
+ // }
+ // }
+
+ for (uint8_t i = 0; i < NUM_MIDIFX_GROUPS; i++)
+ {
+ // Lets them do things in background
+ subModeMidiFx[i].loopUpdate();
+ }
+
+ // Can be modified by scale MidiFX
+ musicScale->calculateScaleIfModified(scaleConfig.scaleRoot, scaleConfig.scalePattern);
+
+ // if (isSubmodeEnabled())
+ // {
+ // activeSubmode->loopUpdate();
+ // }
+}
+
+// Handles selecting params using encoder
+// void OmxModeMidiKeyboard::onEncoderChangedSelectParam(Encoder::Update enc)
+// {
+// if(enc.dir() == 0) return;
+
+// if (enc.dir() < 0) // if turn CCW
+// {
+// params.decrementParam();
+// }
+// else if (enc.dir() > 0) // if turn CW
+// {
+// params.incrementParam();
+// }
+
+// omxDisp.setDirty();
+// }
+
+void OmxModeMidiKeyboard::onEncoderChanged(Encoder::Update enc)
+{
+ if (isSubmodeEnabled())
+ {
+ activeSubmode->onEncoderChanged(enc);
+ return;
+ }
+
+ bool macroConsumesDisplay = false;
+
+ if (macroActive_ && activeMacro_ != nullptr)
+ {
+ macroConsumesDisplay = activeMacro_->consumesDisplay();
+ }
+
+ if (macroConsumesDisplay)
+ {
+ activeMacro_->onEncoderChanged(enc);
+ return;
+ }
+
+ if (encoderSelect && !midiSettings.midiAUX)
+ {
+ // onEncoderChangedSelectParam(enc);
+ params.changeParam(enc.dir());
+ omxDisp.setDirty();
+ return;
+ }
+
+ if (organelleMotherMode)
+ {
+ // CHANGE PAGE
+ if (params.getSelParam() == 0)
+ {
+ if (enc.dir() < 0)
+ { // if turn ccw
+ MM::sendControlChange(CC_OM2, 0, sysSettings.midiChannel);
+ }
+ else if (enc.dir() > 0)
+ { // if turn cw
+ MM::sendControlChange(CC_OM2, 127, sysSettings.midiChannel);
+ }
+ }
+
+ omxDisp.setDirty();
+ }
+
+ // if (midiSettings.midiAUX)
+ // {
+ // // if (enc.dir() < 0)
+ // // { // if turn ccw
+ // // setParam(midiPageParams.miparam - 1);
+ // // omxDisp.setDirty();
+ // // }
+ // // else if (enc.dir() > 0)
+ // // { // if turn cw
+ // // setParam(midiPageParams.miparam + 1);
+ // // omxDisp.setDirty();
+ // // }
+
+ // // change MIDI Background Color
+ // // midiBg_Hue = constrain(midiBg_Hue + (amt * 32), 0, 65534); // 65535
+ // return; // break;
+ // }
+
+ auto amt = enc.accel(5); // where 5 is the acceleration factor if you want it, 0 if you don't)
+
+ int8_t selPage = params.getSelPage();
+ int8_t selParam = params.getSelParam() + 1; // Add one for readability
+
+ if (selPage == MIPAGE_OUTMIDI)
+ {
+ if (selParam == 1)
+ {
+ // set octave
+ midiSettings.octave = constrain(midiSettings.octave + amt, -5, 4);
+ }
+ else if (selParam == 2)
+ {
+ int newchan = constrain(sysSettings.midiChannel + amt, 1, 16);
+ if (newchan != sysSettings.midiChannel) // Is this if necessary?
+ {
+ sysSettings.midiChannel = newchan;
+ }
+ }
+ else if (selParam == 3)
+ {
+ midiSettings.defaultVelocity = constrain((int)midiSettings.defaultVelocity + amt, 0, 127); // cast to int to prevent rollover
+ }
+ }
+ else if (selPage == MIPAGE_OUTCC)
+ {
+ if (selParam == 1)
+ {
+ int newrrchan = constrain(midiSettings.midiRRChannelCount + amt, 1, 16);
+ if (newrrchan != midiSettings.midiRRChannelCount)
+ {
+ midiSettings.midiRRChannelCount = newrrchan;
+ if (midiSettings.midiRRChannelCount == 1)
+ {
+ midiSettings.midiRoundRobin = false;
+ }
+ else
+ {
+ midiSettings.midiRoundRobin = true;
+ }
+ }
+ }
+ else if (selParam == 2)
+ {
+ midiSettings.midiRRChannelOffset = constrain(midiSettings.midiRRChannelOffset + amt, 0, 15);
+ }
+ else if (selParam == 3)
+ {
+ midiSettings.currpgm = constrain(midiSettings.currpgm + amt, 0, 127);
+
+ if (midiSettings.midiRoundRobin)
+ {
+ for (int q = midiSettings.midiRRChannelOffset + 1; q < midiSettings.midiRRChannelOffset + midiSettings.midiRRChannelCount + 1; q++)
+ {
+ MM::sendProgramChange(midiSettings.currpgm, q);
+ }
+ }
+ else
+ {
+ MM::sendProgramChange(midiSettings.currpgm, sysSettings.midiChannel);
+ }
+ }
+ else if (selParam == 4)
+ {
+ midiSettings.currbank = constrain(midiSettings.currbank + amt, 0, 127);
+ // Bank Select is 2 mesages
+ // need to figure out bit shift to get values over 127
+ MM::sendControlChange(0, midiSettings.currbank, sysSettings.midiChannel);
+ MM::sendControlChange(32, 0, sysSettings.midiChannel);
+ MM::sendProgramChange(midiSettings.currpgm, sysSettings.midiChannel);
+ }
+ }
+ else if (selPage == MIPAGE_POTSANDMACROS)
+ {
+ if (selParam == 1)
+ {
+ potSettings.potbank = constrain(potSettings.potbank + amt, 0, NUM_CC_BANKS - 1);
+ // send a CC to the editor here
+ MM::sendControlChange(90, potSettings.potbank, sysSettings.midiChannel);
+ }
+ if (selParam == 2)
+ {
+ midiSettings.midiSoftThru = constrain(midiSettings.midiSoftThru + amt, 0, 1);
+ }
+ if (selParam == 3)
+ {
+ midiMacroConfig.midiMacro = constrain(midiMacroConfig.midiMacro + amt, 0, nummacromodes);
+ }
+ if (selParam == 4)
+ {
+ midiMacroConfig.midiMacroChan = constrain(midiMacroConfig.midiMacroChan + amt, 1, 16);
+ }
+ }
+ else if (selPage == MIPAGE_SCALES)
+ {
+ if (selParam == 1)
+ {
+ int prevRoot = scaleConfig.scaleRoot;
+ scaleConfig.scaleRoot = constrain(scaleConfig.scaleRoot + amt, 0, 12 - 1);
+ if (prevRoot != scaleConfig.scaleRoot)
+ {
+ musicScale->calculateScale(scaleConfig.scaleRoot, scaleConfig.scalePattern);
+ }
+ }
+ if (selParam == 2)
+ {
+ int prevPat = scaleConfig.scalePattern;
+ scaleConfig.scalePattern = constrain(scaleConfig.scalePattern + amt, -1, musicScale->getNumScales() - 1);
+ if (prevPat != scaleConfig.scalePattern)
+ {
+ omxDisp.displayMessage(musicScale->getScaleName(scaleConfig.scalePattern));
+ musicScale->calculateScale(scaleConfig.scaleRoot, scaleConfig.scalePattern);
+ }
+
+ if (scaleConfig.scalePattern == -1)
+ { // record locked and grouped states, then set the current lockScale and group16 to off
+ if (prevPat != -1)
+ {
+ scaleConfig.lockedState = scaleConfig.lockScale;
+ scaleConfig.groupedState = scaleConfig.group16;
+ }
+ scaleConfig.lockScale = 0;
+ scaleConfig.group16 = 0;
+ }
+ else
+ { // restore locked and grouped states if the scale was previously set to off
+ if (prevPat == -1)
+ {
+ scaleConfig.lockScale = scaleConfig.lockedState;
+ scaleConfig.group16 = scaleConfig.groupedState;
+ }
+ }
+ }
+ if (selParam == 3)
+ {
+ if (scaleConfig.scalePattern != -1)
+ {
+ scaleConfig.lockScale = constrain(scaleConfig.lockScale + amt, 0, 1);
+ }
+ }
+ if (selParam == 4)
+ {
+ if (scaleConfig.scalePattern != -1)
+ {
+ scaleConfig.group16 = constrain(scaleConfig.group16 + amt, 0, 1);
+ }
+ }
+ }
+ else if(selPage == MIPAGE_CFG)
+ {
+ if (selParam == 3)
+ {
+ clockConfig.globalQuantizeStepIndex = constrain(clockConfig.globalQuantizeStepIndex + amt, 0, kNumArpRates - 1);
+ }
+ else if (selParam == 4)
+ {
+ cvNoteUtil.triggerMode = constrain(cvNoteUtil.triggerMode + amt, 0, 1);
+ }
+ }
+ else if (selPage == MIPAGE_CLOCK_SOURCE)
+ {
+ if (selParam == 1)
+ {
+ sequencer.clockSource = constrain(sequencer.clockSource + amt, 0, 1);
+ }
+ if (selParam == 2)
+ {
+ clockConfig.send_always = constrain(clockConfig.send_always + amt, 0, 1);
+ }
+ }
+
+
+ omxDisp.setDirty();
+}
+
+void OmxModeMidiKeyboard::onEncoderButtonDown()
+{
+ if (isSubmodeEnabled())
+ {
+ activeSubmode->onEncoderButtonDown();
+ return;
+ }
+
+ bool macroConsumesDisplay = false;
+ if (macroActive_ && activeMacro_ != nullptr)
+ {
+ macroConsumesDisplay = activeMacro_->consumesDisplay();
+ }
+
+ if (macroConsumesDisplay)
+ {
+ activeMacro_->onEncoderButtonDown();
+ return;
+ }
+
+ if(params.getSelPage() == MIPAGE_CFG)
+ {
+ int8_t selParam = params.getSelParam();
+ if(selParam == 0)
+ {
+ enableSubmode(&subModePotConfig_);
+ omxDisp.isDirty();
+ return;
+ }
+ else if(selParam == 1)
+ {
+ enableSubmode(&omxUtil.subModeClearStorage);
+ omxDisp.isDirty();
+ return;
+ }
+ }
+
+ encoderSelect = !encoderSelect;
+ omxDisp.isDirty();
+}
+
+void OmxModeMidiKeyboard::onEncoderButtonUp()
+{
+ if (organelleMotherMode)
+ {
+ // MM::sendControlChange(CC_OM1,0,sysSettings.midiChannel);
+ }
+}
+
+void OmxModeMidiKeyboard::onEncoderButtonDownLong()
+{
+}
+
+bool OmxModeMidiKeyboard::shouldBlockEncEdit()
+{
+ if (isSubmodeEnabled())
+ {
+ return activeSubmode->shouldBlockEncEdit();
+ }
+
+ if (macroActive_)
+ {
+ return true;
+ }
+
+ return false;
+}
+
+void OmxModeMidiKeyboard::onKeyUpdate(OMXKeypadEvent e)
+{
+ if (isSubmodeEnabled())
+ {
+ if (activeSubmode->onKeyUpdate(e))
+ return;
+ }
+
+ int thisKey = e.key();
+
+ // // Aux key debugging
+ // if(thisKey == 0){
+ // const char* dwn = e.down() ? " Down: True" : " Down: False";
+ // Serial.println(String("Clicks: ") + String(e.clicks()) + dwn);
+ // }
+
+ // Aux double click toggle macro
+ if (!isSubmodeEnabled() && midiMacroConfig.midiMacro > 0)
+ {
+ if (!macroActive_)
+ {
+ // Enter M8 Mode
+ if (!e.down() && thisKey == 0 && e.clicks() == 2)
+ {
+ midiSettings.midiAUX = false;
+
+ activeMacro_ = getActiveMacro();
+ if (activeMacro_ != nullptr)
+ {
+ macroActive_ = true;
+ activeMacro_->setEnabled(true);
+ activeMacro_->setScale(musicScale);
+ omxLeds.setDirty();
+ omxDisp.setDirty();
+ return;
+ }
+ // midiMacroConfig.m8AUX = true;
+ return;
+ }
+ }
+ else // Macro mode active
+ {
+ if (!e.down() && thisKey == 0 && e.clicks() == 2)
+ {
+ // exit macro mode
+ if (activeMacro_ != nullptr)
+ {
+ activeMacro_->setEnabled(false);
+ activeMacro_ = nullptr;
+ }
+
+ midiSettings.midiAUX = false;
+ macroActive_ = false;
+ omxLeds.setDirty();
+ omxDisp.setDirty();
+
+ // Clear LEDs
+ for (int m = 1; m < LED_COUNT; m++)
+ {
+ strip.setPixelColor(m, LEDOFF);
+ }
+ }
+ else
+ {
+ if (activeMacro_ != nullptr)
+ {
+ activeMacro_->onKeyUpdate(e);
+ }
+ }
+ return;
+
+ // if(activeMarco_->getEnabled() == false)
+ // {
+ // macroActive_ = false;
+ // midiSettings.midiAUX = false;
+ // activeMarco_ = nullptr;
+
+ // // Clear LEDs
+ // for (int m = 1; m < LED_COUNT; m++)
+ // {
+ // strip.setPixelColor(m, LEDOFF);
+ // }
+ // return;
+ // }
+ // // Exit M8 mode
+ // if (!e.down() && thisKey == 0 && e.clicks() == 2)
+ // {
+ // midiMacroConfig.m8AUX = false;
+ // midiSettings.midiAUX = false;
+ // macroActive_ = true;
+
+ // // Clear LEDs
+ // for (int m = 1; m < LED_COUNT; m++)
+ // {
+ // strip.setPixelColor(m, LEDOFF);
+ // }
+ // return;
+ // }
+
+ // onKeyUpdateM8Macro(e);
+ // return;
+ }
+ }
+
+ if (onKeyUpdateSelMidiFX(e))
+ return;
+
+ // REGULAR KEY PRESSES
+ if (!e.held())
+ { // IGNORE LONG PRESS EVENTS
+ if (e.down() && thisKey != 0)
+ {
+ bool keyConsumed = false; // If used for aux, key will be consumed and not send notes.
+
+ if (midiSettings.midiAUX) // Aux mode
+ {
+ keyConsumed = true;
+
+ if (thisKey == 11 || thisKey == 12) // Change Octave
+ {
+ int amt = thisKey == 11 ? -1 : 1;
+ midiSettings.octave = constrain(midiSettings.octave + amt, -5, 4);
+ }
+ else if (!mfxQuickEdit_ && (thisKey == 1 || thisKey == 2)) // Change Param selection
+ {
+ if (thisKey == 1)
+ {
+ params.decrementParam();
+ }
+ else if (thisKey == 2)
+ {
+ params.incrementParam();
+ }
+ // int chng = thisKey == 1 ? -1 : 1;
+
+ // setParam(constrain((midiPageParams.miparam + chng) % midiPageParams.numParams, 0, midiPageParams.numParams - 1));
+ }
+ // else if(thisKey == 5)
+ // {
+ // // Turn off midiFx
+ // selectMidiFx(127, true);
+ // // mfxIndex = 127;
+ // }
+ // else if (thisKey >= 6 && thisKey < 11)
+ // {
+ // // Change active midiFx
+ // // mfxIndex = thisKey - 6;
+ // selectMidiFx(thisKey - 6, true);
+ // // enableSubmode(&subModeMidiFx[thisKey - 6]);
+ // }
+ // else if(thisKey == 25)
+ // {
+ // if (mfxIndex_ < NUM_MIDIFX_GROUPS)
+ // {
+ // subModeMidiFx[mfxIndex_].toggleArpHold();
+
+ // if (subModeMidiFx[mfxIndex_].isArpHoldOn())
+ // {
+ // omxDisp.displayMessageTimed("Arp Hold: On", 5);
+ // }
+ // else
+ // {
+ // omxDisp.displayMessageTimed("Arp Hold: Off", 5);
+ // }
+ // }
+ // else
+ // {
+ // omxDisp.displayMessageTimed("MidiFX are Off", 5);
+ // }
+ // }
+ // else if(thisKey == 26)
+ // {
+ // if(mfxIndex_ < NUM_MIDIFX_GROUPS)
+ // {
+ // subModeMidiFx[mfxIndex_].toggleArp();
+
+ // if (subModeMidiFx[mfxIndex_].isArpOn())
+ // {
+ // omxDisp.displayMessageTimed("Arp On", 5);
+ // }
+ // else
+ // {
+ // omxDisp.displayMessageTimed("Arp Off", 5);
+ // }
+ // }
+ // else
+ // {
+ // omxDisp.displayMessageTimed("MidiFX are Off", 5);
+ // }
+ // }
+ // else if (e.down() && thisKey == 10)
+ // {
+ // enableSubmode(&subModeMidiFx);
+ // keyConsumed = true;
+ // }
+ // else if (thisKey == 26)
+ // {
+ // keyConsumed = true;
+ // }
+ }
+
+ if (!keyConsumed)
+ {
+ doNoteOn(thisKey);
+ // omxUtil.midiNoteOn(musicScale, thisKey, midiSettings.defaultVelocity, sysSettings.midiChannel);
+ }
+ }
+ else if (!e.down() && thisKey != 0)
+ {
+ doNoteOff(thisKey);
+ // omxUtil.midiNoteOff(thisKey, sysSettings.midiChannel);
+ }
+ }
+ // Serial.println(e.clicks());
+
+ // AUX KEY
+ if (e.down() && thisKey == 0)
+ {
+ // Hard coded Organelle stuff
+ // MM::sendControlChange(CC_AUX, 100, sysSettings.midiChannel);
+
+ // if (!midiMacroConfig.m8AUX)
+ // {
+ // midiSettings.midiAUX = true;
+ // }
+
+ if (!macroActive_)
+ {
+ midiSettings.midiAUX = true;
+ }
+
+ // if (midiAUX) {
+ // // STOP CLOCK
+ // Serial.println("stop clock");
+ // } else {
+ // // START CLOCK
+ // Serial.println("start clock");
+ // }
+ // midiAUX = !midiAUX;
+ }
+ else if (!e.down() && thisKey == 0)
+ {
+ // Hard coded Organelle stuff
+ // MM::sendControlChange(CC_AUX, 0, sysSettings.midiChannel);
+ if (midiSettings.midiAUX)
+ {
+ midiSettings.midiAUX = false;
+ }
+ // turn off leds
+ strip.setPixelColor(0, LEDOFF);
+ strip.setPixelColor(1, LEDOFF);
+ strip.setPixelColor(2, LEDOFF);
+ strip.setPixelColor(11, LEDOFF);
+ strip.setPixelColor(12, LEDOFF);
+ }
+
+ omxLeds.setDirty();
+ omxDisp.setDirty();
+}
+
+bool OmxModeMidiKeyboard::onKeyUpdateSelMidiFX(OMXKeypadEvent e)
+{
+ int thisKey = e.key();
+
+ bool keyConsumed = false;
+
+ if (!e.held())
+ {
+ if (!e.down() && e.clicks() == 2 && thisKey >= 6 && thisKey < 11)
+ {
+ if (midiSettings.midiAUX) // Aux mode
+ {
+ enableSubmode(&subModeMidiFx[thisKey - 6]);
+ keyConsumed = true;
+ }
+ }
+
+ if (e.down() && thisKey != 0)
+ {
+ if (midiSettings.midiAUX) // Aux mode
+ {
+ if (mfxQuickEdit_ && thisKey == 1)
+ {
+ subModeMidiFx[quickEditMfxIndex_].selectPrevMFXSlot();
+ }
+ else if (mfxQuickEdit_ && thisKey == 2)
+ {
+ subModeMidiFx[quickEditMfxIndex_].selectNextMFXSlot();
+ }
+ else if (thisKey == 5)
+ {
+ keyConsumed = true;
+ // Turn off midiFx
+ selectMidiFx(127, true);
+ // mfxIndex_ = 127;
+ }
+ else if (thisKey >= 6 && thisKey < 11)
+ {
+ keyConsumed = true;
+ selectMidiFx(thisKey - 6, true);
+ // Change active midiFx
+ // mfxIndex_ = thisKey - 6;
+ }
+ else if (thisKey == 20) // MidiFX Passthrough
+ {
+ keyConsumed = true;
+ if (mfxIndex_ < NUM_MIDIFX_GROUPS)
+ {
+ enableSubmode(&subModeMidiFx[mfxIndex_]);
+ subModeMidiFx[mfxIndex_].enablePassthrough();
+ mfxQuickEdit_ = true;
+ quickEditMfxIndex_ = mfxIndex_;
+ midiSettings.midiAUX = false;
+ }
+ else
+ {
+ omxDisp.displayMessage(mfxOffMsg);
+ }
+ }
+ else if (thisKey == 22) // Goto arp params
+ {
+ keyConsumed = true;
+ if (mfxIndex_ < NUM_MIDIFX_GROUPS)
+ {
+ enableSubmode(&subModeMidiFx[mfxIndex_]);
+ subModeMidiFx[mfxIndex_].gotoArpParams();
+ midiSettings.midiAUX = false;
+ }
+ else
+ {
+ omxDisp.displayMessage(mfxOffMsg);
+ }
+ }
+ else if (thisKey == 23) // Next arp pattern
+ {
+ keyConsumed = true;
+ if (mfxIndex_ < NUM_MIDIFX_GROUPS)
+ {
+ subModeMidiFx[mfxIndex_].nextArpPattern();
+ }
+ else
+ {
+ omxDisp.displayMessage(mfxOffMsg);
+ }
+ }
+ else if (thisKey == 24) // Next arp octave
+ {
+ keyConsumed = true;
+ if (mfxIndex_ < NUM_MIDIFX_GROUPS)
+ {
+ subModeMidiFx[mfxIndex_].nextArpOctRange();
+ }
+ else
+ {
+ omxDisp.displayMessage(mfxOffMsg);
+ }
+ }
+ else if (thisKey == 25)
+ {
+ keyConsumed = true;
+ if (mfxIndex_ < NUM_MIDIFX_GROUPS)
+ {
+ subModeMidiFx[mfxIndex_].toggleArpHold();
+
+ if (subModeMidiFx[mfxIndex_].isArpHoldOn())
+ {
+ omxDisp.displayMessageTimed("Arp Hold: On", 5);
+ }
+ else
+ {
+ omxDisp.displayMessageTimed("Arp Hold: Off", 5);
+ }
+ }
+ else
+ {
+ omxDisp.displayMessage(mfxOffMsg);
+ }
+ }
+ else if (thisKey == 26)
+ {
+ keyConsumed = true;
+ if (mfxIndex_ < NUM_MIDIFX_GROUPS)
+ {
+ subModeMidiFx[mfxIndex_].toggleArp();
+
+ if (subModeMidiFx[mfxIndex_].isArpOn())
+ {
+ omxDisp.displayMessageTimed("Arp On", 5);
+ }
+ else
+ {
+ omxDisp.displayMessageTimed("Arp Off", 5);
+ }
+ }
+ else
+ {
+ omxDisp.displayMessage(mfxOffMsg);
+ }
+ }
+ }
+ }
+ }
+
+ return keyConsumed;
+}
+
+bool OmxModeMidiKeyboard::onKeyHeldSelMidiFX(OMXKeypadEvent e)
+{
+ int thisKey = e.key();
+
+ bool keyConsumed = false;
+
+ if (midiSettings.midiAUX) // Aux mode
+ {
+ // Enter MidiFX mode
+ if (thisKey >= 6 && thisKey < 11)
+ {
+ keyConsumed = true;
+ enableSubmode(&subModeMidiFx[thisKey - 6]);
+ }
+ }
+
+ return keyConsumed;
+}
+
+void OmxModeMidiKeyboard::onKeyHeldUpdate(OMXKeypadEvent e)
+{
+ if (isSubmodeEnabled())
+ {
+ activeSubmode->onKeyHeldUpdate(e);
+ return;
+ }
+
+ if (onKeyHeldSelMidiFX(e))
+ return;
+
+ // int thisKey = e.key();
+
+ // if (midiSettings.midiAUX) // Aux mode
+ // {
+ // // Enter MidiFX mode
+ // if (thisKey >= 6 && thisKey < 11)
+ // {
+ // enableSubmode(&subModeMidiFx[thisKey - 6]);
+ // }
+ // }
+}
+
+midimacro::MidiMacroInterface *OmxModeMidiKeyboard::getActiveMacro()
+{
+ switch (midiMacroConfig.midiMacro)
+ {
+ case 1:
+ return &m8Macro_;
+ case 2:
+ return &nornsMarco_;
+ case 3:
+ return &delugeMacro_;
+ }
+ return nullptr;
+}
+
+// void OmxModeMidiKeyboard::onKeyUpdateM8Macro(OMXKeypadEvent e)
+// {
+// if (!macroActive_)
+// return;
+// // if (!midiMacroConfig.m8AUX)
+// // return;
+
+// auto activeMacro = getActiveMacro();
+// if(activeMacro == nullptr) return;
+
+// activeMacro->onKeyUpdate(e);
+// }
+
+void OmxModeMidiKeyboard::updateLEDs()
+{
+ if (isSubmodeEnabled())
+ {
+ if (activeSubmode->updateLEDs())
+ return;
+ }
+
+ if (midiSettings.midiAUX)
+ {
+ bool blinkState = omxLeds.getBlinkState();
+
+ // Blink left/right keys for octave select indicators.
+ auto color1 = LIME;
+ auto color2 = MAGENTA;
+
+ for (int q = 1; q < LED_COUNT; q++)
+ {
+ if (midiSettings.midiKeyState[q] == -1)
+ {
+ if (colorConfig.midiBg_Hue == 0)
+ {
+ strip.setPixelColor(q, LEDOFF);
+ }
+ else if (colorConfig.midiBg_Hue == 32)
+ {
+ strip.setPixelColor(q, LOWWHITE);
+ }
+ else
+ {
+ strip.setPixelColor(q, strip.ColorHSV(colorConfig.midiBg_Hue, colorConfig.midiBg_Sat, colorConfig.midiBg_Brightness));
+ }
+ }
+ }
+ strip.setPixelColor(0, RED);
+ strip.setPixelColor(1, color1);
+ strip.setPixelColor(2, color2);
+
+ omxLeds.drawOctaveKeys(11, 12, midiSettings.octave);
+
+ // MidiFX off
+ strip.setPixelColor(5, (mfxIndex_ >= NUM_MIDIFX_GROUPS ? colorConfig.selMidiFXGRPOffColor : colorConfig.midiFXGRPOffColor));
+
+ for (uint8_t i = 0; i < NUM_MIDIFX_GROUPS; i++)
+ {
+ auto mfxColor = (i == mfxIndex_) ? colorConfig.selMidiFXGRPColor : colorConfig.midiFXGRPColor;
+
+ strip.setPixelColor(6 + i, mfxColor);
+ }
+
+ strip.setPixelColor(20, mfxQuickEdit_ && blinkState ? LEDOFF : colorConfig.mfxQuickEdit);
+ strip.setPixelColor(22, colorConfig.gotoArpParams);
+ strip.setPixelColor(23, colorConfig.nextArpPattern);
+
+ if (mfxIndex_ < NUM_MIDIFX_GROUPS)
+ {
+ uint8_t octaveRange = subModeMidiFx[mfxIndex_].getArpOctaveRange();
+ if (octaveRange == 0)
+ {
+ strip.setPixelColor(24, colorConfig.nextArpOctave);
+ }
+ else
+ {
+ // Serial.println("Blink Octave: " + String(octaveRange));
+ bool blinkOctave = omxLeds.getBlinkPattern(octaveRange);
+
+ strip.setPixelColor(24, blinkOctave ? colorConfig.nextArpOctave : LEDOFF);
+ }
+
+ bool isOn = subModeMidiFx[mfxIndex_].isArpOn() && blinkState;
+ bool isHoldOn = subModeMidiFx[mfxIndex_].isArpHoldOn();
+
+ strip.setPixelColor(25, isHoldOn ? colorConfig.arpHoldOn : colorConfig.arpHoldOff);
+ strip.setPixelColor(26, isOn ? colorConfig.arpOn : colorConfig.arpOff);
+ }
+ else
+ {
+ strip.setPixelColor(25, colorConfig.arpHoldOff);
+ strip.setPixelColor(26, colorConfig.arpOff);
+ }
+
+ // strip.setPixelColor(10, color3); // MidiFX key
+
+ // Macros
+ }
+ else
+ {
+ omxLeds.drawMidiLeds(musicScale); // SHOW LEDS
+ }
+
+ if (isSubmodeEnabled())
+ {
+ bool blinkStateSlow = omxLeds.getSlowBlinkState();
+
+ auto auxColor = (blinkStateSlow ? RED : LEDOFF);
+ strip.setPixelColor(0, auxColor);
+ }
+}
+
+void OmxModeMidiKeyboard::onDisplayUpdate()
+{
+ // omxLeds.updateBlinkStates();
+
+ if (isSubmodeEnabled())
+ {
+ if (omxLeds.isDirty())
+ {
+ updateLEDs();
+ }
+ activeSubmode->onDisplayUpdate();
+ return;
+ }
+
+ bool macroConsumesDisplay = false;
+
+ if (macroActive_ && activeMacro_ != nullptr)
+ {
+ activeMacro_->drawLEDs();
+ macroConsumesDisplay = activeMacro_->consumesDisplay();
+ }
+ else
+ {
+ if (omxLeds.isDirty())
+ {
+ updateLEDs();
+ }
+ // if (omxLeds.isDirty())
+ // {
+ // updateLEDs();
+ // // omxLeds.drawMidiLeds(musicScale); // SHOW LEDS
+ // }
+ }
+
+ if (macroConsumesDisplay)
+ {
+ activeMacro_->onDisplayUpdate();
+ }
+ else
+ {
+ if (omxDisp.isDirty())
+ { // DISPLAY
+ if (!encoderConfig.enc_edit)
+ {
+ if (params.getSelPage() == MIPAGE_VERSION)
+ {
+ tempString = "v" + String(MAJOR_VERSION) + "." + String(MINOR_VERSION) + "." + String(POINT_VERSION);
+ omxDisp.dispGenericModeLabel(tempString.c_str(), params.getNumPages(), params.getSelPage());
+ return;
+ }
+
+ if (params.getSelPage() == MIPAGE_OUTMIDI)
+ {
+ omxDisp.clearLegends();
+
+ omxDisp.setLegend(0, "OCT", (int)midiSettings.octave + 4);
+ omxDisp.setLegend(1,"CH", sysSettings.midiChannel);
+ omxDisp.setLegend(2,"VEL", midiSettings.defaultVelocity);
+ }
+ else if (params.getSelPage() == MIPAGE_MIDIINSPECT)
+ {
+ omxDisp.clearLegends();
+
+ omxDisp.setLegend(0,"P CC", potSettings.potCC);
+ omxDisp.setLegend(1,"P VAL", potSettings.potVal);
+ omxDisp.setLegend(2,"NOTE", midiSettings.midiLastNote);
+ omxDisp.setLegend(3,"VEL", midiSettings.midiLastVel);
+ }
+ else if (params.getSelPage() == MIPAGE_OUTCC)
+ {
+ omxDisp.clearLegends();
+
+ omxDisp.setLegend(0,"RR", midiSettings.midiRRChannelCount);
+ omxDisp.setLegend(1,"RROF", midiSettings.midiRRChannelOffset);
+ omxDisp.setLegend(2,"PGM", midiSettings.currpgm + 1);
+ omxDisp.setLegend(3,"BNK", midiSettings.currbank);
+ }
+ else if (params.getSelPage() == MIPAGE_POTSANDMACROS) // SUBMODE_MIDI3
+ {
+ omxDisp.clearLegends();
+
+ omxDisp.setLegend(0,"PBNK", potSettings.potbank + 1);
+ omxDisp.setLegend(1,"THRU", midiSettings.midiSoftThru);
+ omxDisp.setLegend(2,"MCRO", macromodes[midiMacroConfig.midiMacro]);
+ omxDisp.setLegend(3,"M-CH", midiMacroConfig.midiMacroChan);
+ }
+ else if (params.getSelPage() == MIPAGE_SCALES) // SCALES
+ {
+ omxDisp.clearLegends();
+
+ omxDisp.setLegend(0,"ROOT", musicScale->getNoteName(scaleConfig.scaleRoot));
+ omxDisp.setLegend(1,"SCALE", scaleConfig.scalePattern < 0, scaleConfig.scalePattern);
+ omxDisp.setLegend(2,"LOCK", scaleConfig.lockScale);
+ omxDisp.setLegend(3,"GROUP", scaleConfig.group16);
+ }
+ else if (params.getSelPage() == MIPAGE_CFG) // CONFIG
+ {
+ omxDisp.clearLegends();
+
+ omxDisp.setLegend(0,"P CC", "CFG");
+ omxDisp.setLegend(1,"CLR", "STOR");
+ omxDisp.setLegend(2,"QUANT", "1/" + String(kArpRates[clockConfig.globalQuantizeStepIndex]));
+ omxDisp.setLegend(3,"CV M", cvNoteUtil.getTriggerModeDispName());
+ }
+ else if (params.getSelPage() == MIPAGE_CLOCK_SOURCE)
+ {
+ omxDisp.clearLegends();
+
+ omxDisp.setLegend(0,"CLKS", sequencer.clockSource ? "Ext" : "Int");
+ omxDisp.setLegend(1,"SEND", clockConfig.send_always ? "ON" : "OFF"); // Always send clock or not
+ }
+ omxDisp.dispGenericMode2(params.getNumPages(), params.getSelPage(), params.getSelParam(), encoderSelect && !midiSettings.midiAUX);
+ }
+ }
+ }
+}
+
+// incoming midi note on
+void OmxModeMidiKeyboard::inMidiNoteOn(byte channel, byte note, byte velocity)
+{
+ if (organelleMotherMode)
+ return;
+
+ midiSettings.midiLastNote = note;
+ midiSettings.midiLastVel = velocity;
+ int whatoct = (note / 12);
+ int thisKey;
+ uint32_t keyColor = MIDINOTEON;
+
+ if ((whatoct % 2) == 0)
+ {
+ thisKey = note - (12 * whatoct);
+ }
+ else
+ {
+ thisKey = note - (12 * whatoct) + 12;
+ }
+ if (whatoct == 0)
+ { // ORANGE,YELLOW,GREEN,MAGENTA,CYAN,BLUE,LIME,LTPURPLE
+ }
+ else if (whatoct == 1)
+ {
+ keyColor = ORANGE;
+ }
+ else if (whatoct == 2)
+ {
+ keyColor = YELLOW;
+ }
+ else if (whatoct == 3)
+ {
+ keyColor = GREEN;
+ }
+ else if (whatoct == 4)
+ {
+ keyColor = MAGENTA;
+ }
+ else if (whatoct == 5)
+ {
+ keyColor = CYAN;
+ }
+ else if (whatoct == 6)
+ {
+ keyColor = LIME;
+ }
+ else if (whatoct == 7)
+ {
+ keyColor = CYAN;
+ }
+ strip.setPixelColor(midiKeyMap[thisKey], keyColor); // Set pixel's color (in RAM)
+ // dirtyPixels = true;
+ strip.show();
+ omxDisp.setDirty();
+}
+
+void OmxModeMidiKeyboard::inMidiNoteOff(byte channel, byte note, byte velocity)
+{
+ if (organelleMotherMode)
+ return;
+
+ int whatoct = (note / 12);
+ int thisKey;
+ if ((whatoct % 2) == 0)
+ {
+ thisKey = note - (12 * whatoct);
+ }
+ else
+ {
+ thisKey = note - (12 * whatoct) + 12;
+ }
+ strip.setPixelColor(midiKeyMap[thisKey], LEDOFF); // Set pixel's color (in RAM)
+ // dirtyPixels = true;
+ strip.show();
+ omxDisp.setDirty();
+}
+
+void OmxModeMidiKeyboard::inMidiControlChange(byte channel, byte control, byte value)
+{
+ auto activeMacro = getActiveMacro();
+
+ if (activeMacro != nullptr)
+ {
+ activeMacro->inMidiControlChange(channel, control, value);
+ }
+}
+
+void OmxModeMidiKeyboard::SetScale(MusicScales *scale)
+{
+ this->musicScale = scale;
+ m8Macro_.setScale(scale);
+ nornsMarco_.setScale(scale);
+}
+
+void OmxModeMidiKeyboard::sendMidiClock(bool send)
+{
+ clockConfig.send_always = !clockConfig.send_always;
+}
+
+void OmxModeMidiKeyboard::enableSubmode(SubmodeInterface *subMode)
+{
+ if (activeSubmode != nullptr)
+ {
+ activeSubmode->setEnabled(false);
+ }
+
+ activeSubmode = subMode;
+ activeSubmode->setEnabled(true);
+ omxDisp.setDirty();
+}
+
+void OmxModeMidiKeyboard::disableSubmode()
+{
+ if (activeSubmode != nullptr)
+ {
+ activeSubmode->setEnabled(false);
+ }
+
+ midiSettings.midiAUX = false;
+ mfxQuickEdit_ = false;
+ activeSubmode = nullptr;
+ omxDisp.setDirty();
+}
+
+bool OmxModeMidiKeyboard::isSubmodeEnabled()
+{
+ if (activeSubmode == nullptr)
+ return false;
+
+ if (activeSubmode->isEnabled() == false)
+ {
+ disableSubmode();
+ midiSettings.midiAUX = false;
+ return false;
+ }
+
+ return true;
+}
+
+void OmxModeMidiKeyboard::doNoteOn(uint8_t keyIndex)
+{
+ MidiNoteGroup noteGroup = omxUtil.midiNoteOn2(musicScale, keyIndex, midiSettings.defaultVelocity, sysSettings.midiChannel);
+
+ if (noteGroup.noteNumber == 255)
+ return;
+
+ // Serial.println("doNoteOn: " + String(noteGroup.noteNumber));
+
+ noteGroup.unknownLength = true;
+ noteGroup.prevNoteNumber = noteGroup.noteNumber;
+
+ if (mfxIndex_ < NUM_MIDIFX_GROUPS)
+ {
+ subModeMidiFx[mfxIndex_].noteInput(noteGroup);
+ // subModeMidiFx.noteInput(noteGroup);
+ }
+ else
+ {
+ onNotePostFX(noteGroup);
+ }
+}
+void OmxModeMidiKeyboard::doNoteOff(uint8_t keyIndex)
+{
+ MidiNoteGroup noteGroup = omxUtil.midiNoteOff2(keyIndex, sysSettings.midiChannel);
+
+ if (noteGroup.noteNumber == 255)
+ return;
+
+ // Serial.println("doNoteOff: " + String(noteGroup.noteNumber));
+
+ noteGroup.unknownLength = true;
+ noteGroup.prevNoteNumber = noteGroup.noteNumber;
+
+ if (mfxIndex_ < NUM_MIDIFX_GROUPS)
+ {
+ subModeMidiFx[mfxIndex_].noteInput(noteGroup);
+ // subModeMidiFx.noteInput(noteGroup);
+ }
+ else
+ {
+ onNotePostFX(noteGroup);
+ }
+}
+
+// // Called by a euclid sequencer when it triggers a note
+// void OmxModeMidiKeyboard::onNoteTriggered(uint8_t euclidIndex, MidiNoteGroup note)
+// {
+// // Serial.println("OmxModeEuclidean::onNoteTriggered " + String(euclidIndex) + " note: " + String(note.noteNumber));
+
+// subModeMidiFx.noteInput(note);
+
+// omxDisp.setDirty();
+// }
+
+// Called by the midiFX group when a note exits it's FX Pedalboard
+void OmxModeMidiKeyboard::onNotePostFX(MidiNoteGroup note)
+{
+ if (note.noteOff)
+ {
+ // Serial.println("OmxModeMidiKeyboard::onNotePostFX noteOff: " + String(note.noteNumber));
+
+ if (note.sendMidi)
+ {
+ MM::sendNoteOff(note.noteNumber, note.velocity, note.channel);
+ }
+ if (note.sendCV)
+ {
+ cvNoteUtil.cvNoteOff(note.noteNumber);
+ }
+ }
+ else
+ {
+ if (note.unknownLength == false)
+ {
+ uint32_t noteOnMicros = note.noteonMicros; // TODO Might need to be set to current micros
+ pendingNoteOns.insert(note.noteNumber, note.velocity, note.channel, noteOnMicros, note.sendCV);
+
+ // Serial.println("StepLength: " + String(note.stepLength));
+
+ uint32_t noteOffMicros = noteOnMicros + (note.stepLength * clockConfig.step_micros);
+ pendingNoteOffs.insert(note.noteNumber, note.channel, noteOffMicros, note.sendCV);
+
+ // Serial.println("noteOnMicros: " + String(noteOnMicros));
+ // Serial.println("noteOffMicros: " + String(noteOffMicros));
+ }
+ else
+ {
+ // Serial.println("OmxModeMidiKeyboard::onNotePostFX noteOn: " + String(note.noteNumber));
+
+ if (note.sendMidi)
+ {
+ midiSettings.midiLastNote = note.noteNumber;
+ midiSettings.midiLastVel = note.velocity;
+ MM::sendNoteOn(note.noteNumber, note.velocity, note.channel);
+ }
+ if (note.sendCV)
+ {
+ cvNoteUtil.cvNoteOn(note.noteNumber);
+ }
+ }
+ }
+
+ // uint32_t noteOnMicros = note.noteonMicros; // TODO Might need to be set to current micros
+ // pendingNoteOns.insert(note.noteNumber, note.velocity, note.channel, noteOnMicros, note.sendCV);
+
+ // uint32_t noteOffMicros = noteOnMicros + (note.stepLength * clockConfig.step_micros);
+ // pendingNoteOffs.insert(note.noteNumber, note.channel, noteOffMicros, note.sendCV);
+}
+
+void OmxModeMidiKeyboard::onPendingNoteOff(int note, int channel)
+{
+ // Serial.println("OmxModeEuclidean::onPendingNoteOff " + String(note) + " " + String(channel));
+ // subModeMidiFx.onPendingNoteOff(note, channel);
+
+ for (uint8_t i = 0; i < NUM_MIDIFX_GROUPS; i++)
+ {
+ subModeMidiFx[i].onPendingNoteOff(note, channel);
+ }
+}
diff --git a/Archive/OMX-27-firmware/src/modes/omx_mode_midi_keyboard.h b/Archive/OMX-27-firmware/src/modes/omx_mode_midi_keyboard.h
new file mode 100644
index 00000000..8eed62c8
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/modes/omx_mode_midi_keyboard.h
@@ -0,0 +1,144 @@
+#pragma once
+
+#include "omx_mode_interface.h"
+#include "../utils/music_scales.h"
+#include "../utils/param_manager.h"
+#include "submodes/submode_midifxgroup.h"
+#include "submodes/submode_potconfig.h"
+#include "../midifx/midifx_interface.h"
+#include "../midifx/midifx_interface.h"
+#include "../midimacro/midimacro_m8.h"
+#include "../midimacro/midimacro_norns.h"
+#include "../midimacro/midimacro_deluge.h"
+
+class OmxModeMidiKeyboard : public OmxModeInterface
+{
+public:
+ OmxModeMidiKeyboard();
+ ~OmxModeMidiKeyboard() {}
+
+ void InitSetup() override;
+ void onModeActivated() override;
+ void onModeDeactivated() override;
+
+ void setOrganelleMode()
+ {
+ organelleMotherMode = true;
+ }
+
+ void setMidiMode()
+ {
+ organelleMotherMode = false;
+ }
+
+ void onPotChanged(int potIndex, int prevValue, int newValue, int analogDelta) override;
+ void loopUpdate(Micros elapsedTime) override;
+ void onClockTick() override;
+
+ void updateLEDs() override;
+
+ void onEncoderChanged(Encoder::Update enc) override;
+ void onEncoderButtonDown() override;
+ void onEncoderButtonUp() override;
+
+ void onEncoderButtonDownLong() override;
+
+ bool shouldBlockEncEdit() override;
+
+ void onKeyUpdate(OMXKeypadEvent e) override;
+ void onKeyHeldUpdate(OMXKeypadEvent e) override;
+
+ void onDisplayUpdate() override;
+ void inMidiNoteOn(byte channel, byte note, byte velocity) override;
+ void inMidiNoteOff(byte channel, byte note, byte velocity) override;
+ void inMidiControlChange(byte channel, byte control, byte value) override;
+
+ void sendMidiClock(bool send);
+
+ void SetScale(MusicScales *scale);
+
+private:
+ bool initSetup = false;
+ bool organelleMotherMode = false; // TODO make separate class for this
+
+ MusicScales *musicScale;
+
+ void changePage(int amt);
+ void setParam(int paramIndex);
+
+ void onKeyUpdateM8Macro(OMXKeypadEvent e);
+ bool onKeyUpdateSelMidiFX(OMXKeypadEvent e);
+ bool onKeyHeldSelMidiFX(OMXKeypadEvent e);
+
+ // If true, encoder selects param rather than modifies value
+ bool encoderSelect = false;
+ // void onEncoderChangedSelectParam(Encoder::Update enc);
+ ParamManager params;
+
+ bool macroActive_ = false;
+ bool mfxQuickEdit_ = false;
+
+ // SubModes
+ SubmodeInterface *activeSubmode = nullptr;
+ // SubModeMidiFxGroup subModeMidiFx;
+ SubModePotConfig subModePotConfig_;
+
+ void enableSubmode(SubmodeInterface *subMode);
+ void disableSubmode();
+ bool isSubmodeEnabled();
+
+ // // Static glue to link a pointer to a member function
+ // static void onNoteTriggeredForwarder(void *context, uint8_t euclidIndex, MidiNoteGroup note)
+ // {
+ // static_cast(context)->onNoteTriggered(euclidIndex, note);
+ // }
+
+ void doNoteOn(uint8_t keyIndex);
+ void doNoteOff(uint8_t keyIndex);
+
+ // void onNoteTriggered(MidiNoteGroup note);
+ // void onNoteOffTriggered(MidiNoteGroup note);
+
+ // Static glue to link a pointer to a member function
+ static void onNotePostFXForwarder(void *context, MidiNoteGroup note)
+ {
+ static_cast(context)->onNotePostFX(note);
+ }
+
+ void onNotePostFX(MidiNoteGroup note);
+
+ // Static glue to link a pointer to a member function
+ static void onPendingNoteOffForwarder(void *context, int note, int channel)
+ {
+ static_cast(context)->onPendingNoteOff(note, channel);
+ }
+
+ void onPendingNoteOff(int note, int channel);
+
+ void stopSequencers();
+
+ void selectMidiFx(uint8_t mfxIndex, bool dispMsg);
+
+ uint8_t mfxIndex_ = 0;
+ uint8_t quickEditMfxIndex_ = 0;
+
+ midimacro::MidiMacroNorns nornsMarco_;
+ midimacro::MidiMacroM8 m8Macro_;
+ midimacro::MidiMacroDeluge delugeMacro_;
+
+ midimacro::MidiMacroInterface *activeMacro_;
+
+ midimacro::MidiMacroInterface *getActiveMacro();
+
+ // Static glue to link a pointer to a member function
+ static void doNoteOnForwarder(void *context, uint8_t keyIndex)
+ {
+ static_cast(context)->doNoteOn(keyIndex);
+ }
+
+ // Static glue to link a pointer to a member function
+ static void doNoteOffForwarder(void *context, uint8_t keyIndex)
+ {
+ static_cast(context)->doNoteOff(keyIndex);
+ }
+};
diff --git a/Archive/OMX-27-firmware/src/modes/omx_mode_sequencer.cpp b/Archive/OMX-27-firmware/src/modes/omx_mode_sequencer.cpp
new file mode 100644
index 00000000..0b9789f0
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/modes/omx_mode_sequencer.cpp
@@ -0,0 +1,1631 @@
+#include "omx_mode_sequencer.h"
+#include "../config.h"
+#include "../consts/colors.h"
+#include "../utils/omx_util.h"
+#include "../hardware/omx_disp.h"
+#include "sequencer.h"
+#include "../hardware/omx_leds.h"
+
+enum SequencerMode
+{
+ SEQMODE_MAIN,
+ SEQMODE_NOTESEL,
+ SEQMODE_PAT,
+ SEQMODE_STEPRECORD
+};
+
+StepNote stepCopyBuffer_;
+// String tempString_;
+
+OmxModeSequencer::OmxModeSequencer()
+{
+ // seq params
+ seqParams.addPage(4);
+ seqParams.addPage(4);
+
+ // note select params
+ noteSelParams.addPage(4);
+ noteSelParams.addPage(4);
+ noteSelParams.addPage(4);
+
+ // pattern params
+ patParams.addPage(4);
+ patParams.addPage(4);
+ patParams.addPage(4);
+
+ // step record params
+ sRecParams.addPage(4);
+ sRecParams.addPage(4);
+}
+
+void OmxModeSequencer::InitSetup()
+{
+ initSetup = true;
+}
+
+void OmxModeSequencer::onModeActivated()
+{
+ if (!initSetup)
+ {
+ InitSetup();
+ }
+
+ changeSequencerMode(SEQMODE_MAIN);
+}
+
+uint8_t OmxModeSequencer::getAdjustedNote(uint8_t keyNumber)
+{
+ uint8_t adjnote = notes[keyNumber] + (midiSettings.octave * 12);
+ return adjnote;
+}
+
+// Set state defaults when changing modes
+// Helps keep things from getting in weird states and makes code more readable
+void OmxModeSequencer::changeSequencerMode(uint8_t newMode)
+{
+ // Serial.println((String)"changeSequencerMode: " + String((SequencerMode)newMode));
+ noteSelect_ = false;
+ // noteSelection_ = false;
+ // stepSelect_ = false;
+
+ stepRecord_ = false;
+ patternParams_ = false;
+
+ switch (newMode)
+ {
+ case SEQMODE_MAIN:
+ {
+ seqParams.setSelPageAndParam(0, 0);
+ encoderSelect_ = true;
+ }
+ break;
+ case SEQMODE_NOTESEL:
+ {
+ noteSelect_ = true;
+ // stepSelect_ = true;
+ // noteSelection_ = true;
+ noteSelParams.setSelPageAndParam(0, 0);
+ encoderSelect_ = false;
+ omxDisp.displayMessagef("NOTE SELECT");
+ }
+ break;
+ case SEQMODE_PAT:
+ {
+ patternParams_ = true;
+ patParams.setSelPageAndParam(0, 1);
+ encoderSelect_ = false;
+ omxDisp.displayMessagef("PATT PARAMS");
+ }
+ break;
+ case SEQMODE_STEPRECORD:
+ {
+ stepRecord_ = true;
+ sRecParams.setSelPageAndParam(0, 1);
+ encoderSelect_ = false;
+ omxDisp.displayMessagef("STEP RECORD");
+ }
+ break;
+ default:
+ break;
+ }
+
+ omxDisp.setDirty();
+ omxLeds.setDirty();
+}
+
+uint8_t OmxModeSequencer::getSequencerMode()
+{
+ if (noteSelect_)
+ {
+ return SEQMODE_NOTESEL;
+ }
+ else if (patternParams_)
+ {
+ return SEQMODE_PAT;
+ }
+ else if (stepRecord_)
+ {
+ return SEQMODE_STEPRECORD;
+ }
+
+ return SEQMODE_MAIN;
+}
+
+void OmxModeSequencer::onPotChanged(int potIndex, int prevValue, int newValue, int analogDelta)
+{
+ uint8_t seqMode = getSequencerMode();
+
+ // note selection - do P-Locks
+ if (seqMode == SEQMODE_NOTESEL)
+ {
+ potSettings.potNum = potIndex;
+ potSettings.potCC = pots[potSettings.potbank][potIndex];
+ potSettings.potVal = potSettings.analogValues[potIndex];
+
+ if (potIndex < 4)
+ { // only store p-lock value for first 4 knobs
+ getSelectedStep()->params[potIndex] = potSettings.analogValues[potIndex];
+ omxUtil.sendPots(potIndex, sequencer.getPatternChannel(sequencer.playingPattern));
+ }
+ omxUtil.sendPots(potIndex, sequencer.getPatternChannel(sequencer.playingPattern));
+ omxDisp.setDirty();
+ }
+ else if (seqMode == SEQMODE_STEPRECORD)
+ {
+ potSettings.potNum = potIndex;
+ potSettings.potCC = pots[potSettings.potbank][potIndex];
+ potSettings.potVal = potSettings.analogValues[potIndex];
+
+ if (potIndex < 4)
+ { // only store p-lock value for first 4 knobs
+ sequencer.getCurrentPattern()->steps[sequencer.seqPos[sequencer.playingPattern]].params[potIndex] = potSettings.analogValues[potIndex];
+ omxUtil.sendPots(potIndex, sequencer.getPatternChannel(sequencer.playingPattern));
+ }
+ else if (potIndex == 4)
+ {
+ sequencer.getCurrentPattern()->steps[sequencer.seqPos[sequencer.playingPattern]].vel = potSettings.analogValues[potIndex]; // SET POT 5 to NOTE VELOCITY HERE
+ }
+ omxDisp.setDirty();
+ }
+ else if (seqMode == SEQMODE_MAIN || seqMode == SEQMODE_PAT)
+ {
+ omxUtil.sendPots(potIndex, sequencer.getPatternChannel(sequencer.playingPattern));
+ }
+}
+
+void OmxModeSequencer::loopUpdate(Micros elapsedTime)
+{
+ if (!seq2Mode) // S1
+ {
+ doStepS1();
+ }
+ else // S2
+ {
+ doStepS2();
+ }
+
+ // renders leds for the playing pattern
+ updateLEDs();
+}
+
+// Handles selecting params using encoder
+void OmxModeSequencer::onEncoderChangedSelectParam(Encoder::Update enc)
+{
+ if (enc.dir() == 0)
+ return;
+
+ uint8_t seqMode = getSequencerMode();
+
+ if (seqMode == SEQMODE_MAIN)
+ {
+ seqParams.changeParam(enc.dir());
+ }
+ else if (seqMode == SEQMODE_NOTESEL)
+ {
+ noteSelParams.changeParam(enc.dir());
+ }
+ else if (seqMode == SEQMODE_PAT)
+ {
+ patParams.changeParam(enc.dir());
+ }
+ else if (seqMode == SEQMODE_STEPRECORD)
+ {
+ sRecParams.changeParam(enc.dir());
+ }
+
+ omxDisp.setDirty();
+}
+
+void OmxModeSequencer::onEncoderChanged(Encoder::Update enc)
+{
+ if (encoderSelect_)
+ {
+ onEncoderChangedSelectParam(enc);
+ }
+ else
+ {
+ if (getSequencerMode() == SEQMODE_MAIN)
+ {
+ onEncoderChangedNorm(enc);
+ }
+ else
+ {
+ onEncoderChangedStep(enc);
+ }
+ }
+}
+
+void OmxModeSequencer::onEncoderChangedNorm(Encoder::Update enc)
+{
+ auto amt = enc.accel(5); // where 5 is the acceleration factor if you want it, 0 if you don't)
+
+ int8_t selPage = seqParams.getSelPage() + 1; // Add one for readability
+ int8_t selParam = seqParams.getSelParam() + 1;
+
+ // PAGE ONE
+ if (selPage == 1)
+ {
+ if (selParam == 1) // CHANGE PATTERN
+ {
+ sequencer.playingPattern = constrain(sequencer.playingPattern + amt, 0, 7);
+ if (sequencer.getCurrentPattern()->solo)
+ {
+ omxLeds.setAllLEDS(0, 0, 0);
+ }
+ }
+ else if (selParam == 2) // SET TRANSPOSE
+ {
+ transposeSeq(sequencer.playingPattern, amt); //
+ int newtransp = constrain(midiSettings.transpose + amt, -64, 63);
+ midiSettings.transpose = newtransp;
+ }
+ else if (selParam == 3) // SET SWING
+ {
+ int newswing = constrain(sequencer.getCurrentPattern()->swing + amt, 0, midiSettings.maxswing - 1); // -1 to deal with display values
+ midiSettings.swing = newswing;
+ sequencer.getCurrentPattern()->swing = newswing;
+ // setGlobalSwing(newswing);
+ }
+ else if (selParam == 4) // SET TEMPO
+ {
+ clockConfig.newtempo = constrain(clockConfig.clockbpm + amt, 40, 300);
+ if (clockConfig.newtempo != clockConfig.clockbpm)
+ {
+ // SET TEMPO HERE
+ clockConfig.clockbpm = clockConfig.newtempo;
+ omxUtil.resetClocks();
+ }
+ }
+ }
+ // PAGE TWO
+ else if (selPage == 2)
+ {
+ if (selParam == 1) // MIDI SOLO
+ {
+ // playingPattern = constrain(playingPattern + amt, 0, 7);
+ sequencer.getCurrentPattern()->solo = constrain(sequencer.getCurrentPattern()->solo + amt, 0, 1);
+ if (sequencer.getCurrentPattern()->solo)
+ {
+ omxLeds.setAllLEDS(0, 0, 0);
+ }
+ }
+ else if (selParam == 2) // SET PATTERN LENGTH
+ {
+ auto newPatternLen = constrain(sequencer.getPatternLength(sequencer.playingPattern) + amt, 1, NUM_STEPS);
+ sequencer.setPatternLength(sequencer.playingPattern, newPatternLen);
+ if (sequencer.seqPos[sequencer.playingPattern] >= newPatternLen)
+ {
+ sequencer.seqPos[sequencer.playingPattern] = newPatternLen - 1;
+ sequencer.patternPage[sequencer.playingPattern] = getPatternPage(sequencer.seqPos[sequencer.playingPattern]);
+ }
+ }
+ else if (selParam == 3) // SET CLOCK DIV/MULT
+ {
+ sequencer.getCurrentPattern()->clockDivMultP = constrain(sequencer.getCurrentPattern()->clockDivMultP + amt, 0, NUM_MULTDIVS - 1);
+ }
+ else if (selParam == 4) // SET CV ON/OFF
+ {
+ sequencer.getCurrentPattern()->sendCV = constrain(sequencer.getCurrentPattern()->sendCV + amt, 0, 1);
+ }
+ }
+ omxDisp.setDirty();
+}
+
+// TODO: break this into separate functions
+void OmxModeSequencer::onEncoderChangedStep(Encoder::Update enc)
+{
+ auto amt = enc.accel(5); // where 5 is the acceleration factor if you want it, 0 if you don't)
+ auto amtSlow = enc.accel(1);
+
+ uint8_t seqMode = getSequencerMode();
+
+ // SEQUENCE PATTERN PARAMS SUB MODE
+ if (seqMode == SEQMODE_PAT)
+ {
+ int8_t selPage = patParams.getSelPage() + 1; // Add one for readability
+ int8_t selParam = patParams.getSelParam() + 1;
+
+ // PAGE ONE
+ if (selPage == 1)
+ {
+ if (selParam == 1) // SET PLAYING PATTERN
+ {
+ sequencer.playingPattern = constrain(sequencer.playingPattern + amt, 0, 7);
+ }
+ if (selParam == 2) // SET LENGTH
+ {
+ auto newPatternLen = constrain(sequencer.getPatternLength(sequencer.playingPattern) + amt, 1, NUM_STEPS);
+ sequencer.setPatternLength(sequencer.playingPattern, newPatternLen);
+ if (sequencer.seqPos[sequencer.playingPattern] >= newPatternLen)
+ {
+ sequencer.seqPos[sequencer.playingPattern] = newPatternLen - 1;
+ sequencer.patternPage[sequencer.playingPattern] = getPatternPage(sequencer.seqPos[sequencer.playingPattern]);
+ }
+ }
+ if (selParam == 3) // SET PATTERN ROTATION
+ {
+ int rotator;
+ (enc.dir() < 0 ? rotator = -1 : rotator = 1);
+ // int rotator = constrain(rotcc, (sequencer.PatternLength(sequencer.playingPattern))*-1, sequencer.PatternLength(sequencer.playingPattern));
+ midiSettings.rotationAmt = midiSettings.rotationAmt + rotator;
+ if (midiSettings.rotationAmt < 16 && midiSettings.rotationAmt > -16)
+ { // NUM_STEPS??
+ rotatePattern(sequencer.playingPattern, rotator);
+ }
+ midiSettings.rotationAmt = constrain(midiSettings.rotationAmt, (sequencer.getPatternLength(sequencer.playingPattern) - 1) * -1, sequencer.getPatternLength(sequencer.playingPattern) - 1);
+ }
+ if (selParam == 4) // SET PATTERN CHANNEL
+ {
+ sequencer.getCurrentPattern()->channel = constrain(sequencer.getCurrentPattern()->channel + amt, 0, 15);
+ }
+ }
+ // PATTERN PARAMS PAGE 2
+ else if (selPage == 2)
+ {
+ if (selParam == 1) // SET AUTO START STEP
+ {
+ sequencer.getCurrentPattern()->startstep = constrain(sequencer.getCurrentPattern()->startstep + amt, 0, sequencer.getCurrentPattern()->len);
+ // sequencer.getCurrentPattern()->startstep--;
+ }
+ if (selParam == 2) // SET AUTO RESET STEP
+ {
+ int tempresetstep = sequencer.getCurrentPattern()->autoresetstep + amt;
+ sequencer.getCurrentPattern()->autoresetstep = constrain(tempresetstep, 0, sequencer.getCurrentPattern()->len + 1);
+ }
+ if (selParam == 3) // SET AUTO RESET FREQUENCY
+ {
+ sequencer.getCurrentPattern()->autoresetfreq = constrain(sequencer.getCurrentPattern()->autoresetfreq + amt, 0, 15); // max every 16 times
+ }
+ if (selParam == 4) // SET AUTO RESET PROB
+ {
+ sequencer.getCurrentPattern()->autoresetprob = constrain(sequencer.getCurrentPattern()->autoresetprob + amt, 0, 100); // never, 100% - 33%
+ }
+ }
+ // PAGE THREE
+ else if (selPage == 3)
+ {
+ if (selParam == 1) // SET CLOCK-DIV-MULT
+ {
+ sequencer.getCurrentPattern()->clockDivMultP = constrain(sequencer.getCurrentPattern()->clockDivMultP + amt, 0, NUM_MULTDIVS - 1); // set clock div/mult
+ }
+ if (selParam == 2) // SET MIDI SOLO
+ {
+ sequencer.getCurrentPattern()->solo = constrain(sequencer.getCurrentPattern()->solo + amt, 0, 1);
+ }
+ }
+ }
+ // STEP RECORD SUB MODE
+ else if (seqMode == SEQMODE_STEPRECORD)
+ {
+ int8_t selPage = sRecParams.getSelPage() + 1; // Add one for readability
+ int8_t selParam = sRecParams.getSelParam() + 1;
+
+ // PAGE ONE
+ if (selPage == 1)
+ {
+ if (selParam == 1) // OCTAVE SELECTION
+ {
+ midiSettings.octave = constrain(midiSettings.octave + amt, -5, 4);
+ }
+ if (selParam == 2) // STEP SELECTION
+ {
+ if (enc.dir() > 0)
+ {
+ step_ahead();
+ }
+ else if (enc.dir() < 0)
+ {
+ step_back();
+ }
+ seqConfig.selectedStep = sequencer.seqPos[sequencer.playingPattern];
+ }
+ if (selParam == 3) // SET NOTE NUM
+ {
+ int tempNote = getSelectedStep()->note;
+ getSelectedStep()->note = constrain(tempNote + amt, 0, 127);
+ }
+ if (selParam == 4) // Pattern
+ {
+ // playingPattern = constrain(playingPattern + amt, 0, 7);
+ }
+ }
+ // PAGE TWO
+ else if (selPage == 2)
+ {
+ if (selParam == 1) // STEP TYPE
+ {
+ changeStepType(amt);
+ }
+ if (selParam == 2) // STEP PROB
+ {
+ int tempProb = getSelectedStep()->prob;
+ getSelectedStep()->prob = constrain(tempProb + amt, 0, 100); // Note Len between 1-16
+ }
+ if (selParam == 3) // STEP CONDITION
+ {
+ int tempCondition = getSelectedStep()->condition;
+ getSelectedStep()->condition = constrain(tempCondition + amt, 0, 35); // 0-32
+ }
+ }
+ }
+ // NOTE SELECT MODE
+ else if (seqMode == SEQMODE_NOTESEL)
+ {
+ int8_t selPage = noteSelParams.getSelPage() + 1; // Add one for readability
+ int8_t selParam = noteSelParams.getSelParam() + 1;
+
+ // PAGE ONE
+ if (selPage == 1)
+ {
+ if (selParam == 1) // SET NOTE NUM
+ {
+ int tempNote = getSelectedStep()->note;
+ getSelectedStep()->note = constrain(tempNote + amt, 0, 127);
+ }
+ if (selParam == 2) // SET OCTAVE
+ {
+ midiSettings.octave = constrain(midiSettings.octave + amt, -5, 4);
+ }
+ if (selParam == 3) // SET VELOCITY
+ {
+ int tempVel = getSelectedStep()->vel;
+ getSelectedStep()->vel = constrain(tempVel + amt, 0, 127);
+ }
+ if (selParam == 4) // SET NOTE LENGTH
+ {
+ auto step = getSelectedStep();
+
+ step->len = constrain(step->len + amtSlow, 0, kNumNoteLengths - 1); // Note Len between 1-16
+
+ // int tempLen = step->len;
+ // // int newLen = tempLen + amtSlow;
+ // auto newLen = constrain(step->len + amtSlow, 0, kNumNoteLengths - 1); // Note Len between 1-16
+ // step->len = (uint8_t)newLen; // Note Len between 1-16
+
+ // Serial.println("amtSlow = " + String(amtSlow));
+ // Serial.println("tempLen = " + String(tempLen));
+ // Serial.println("newLen = " + String(newLen));
+ // Serial.println("len = " + String(step->len));
+ // Serial.println("NumNoteLengths = " + String(kNumNoteLengths));
+ // Serial.println("NoteLength = " + String(kNoteLengths[step->len]));
+ }
+ }
+ // PAGE TWO
+ else if (selPage == 2)
+ {
+ if (noteSelParams.getSelParam() == 0) // SET STEP TYPE
+ {
+ changeStepType(amt);
+ }
+ if (noteSelParams.getSelParam() == 1) // SET STEP PROB
+ {
+ int tempProb = getSelectedStep()->prob;
+ getSelectedStep()->prob = constrain(tempProb + amt, 0, 100); // Note Len between 1-16
+ }
+ if (noteSelParams.getSelParam() == 2) // SET STEP TRIG CONDITION
+ {
+ int tempCondition = getSelectedStep()->condition;
+ getSelectedStep()->condition = constrain(tempCondition + amt, 0, 35); // 0-32
+ }
+ }
+ // PAGE THREE
+ else if (selPage == 3)
+ {
+ if (enc.dir() < 0)
+ { // RESET PLOCK IF TURN CCW
+ // int tempmode = seqPageParams.nsparam - 11;
+ int tempmode = noteSelParams.getSelParam();
+ getSelectedStep()->params[tempmode] = -1;
+ }
+ }
+ }
+ else
+ {
+ // TODO This shouldn't be possible.
+ clockConfig.newtempo = constrain(clockConfig.clockbpm + amt, 40, 300);
+ if (clockConfig.newtempo != clockConfig.clockbpm)
+ {
+ // SET TEMPO HERE
+ clockConfig.clockbpm = clockConfig.newtempo;
+ omxUtil.resetClocks();
+ }
+ }
+ omxDisp.setDirty();
+}
+
+void OmxModeSequencer::onEncoderButtonDown()
+{
+ encoderSelect_ = !encoderSelect_;
+ omxDisp.isDirty();
+}
+
+void OmxModeSequencer::onEncoderButtonDownLong()
+{
+ if (getSequencerMode() == SEQMODE_STEPRECORD)
+ {
+ resetPatternDefaults(sequencer.playingPattern);
+ omxDisp.displayMessagef("RESET PAT");
+ omxDisp.setDirty();
+ // clearedFlag = true;
+ }
+}
+
+bool OmxModeSequencer::shouldBlockEncEdit()
+{
+ return stepRecord_;
+}
+
+void OmxModeSequencer::onKeyUpdate(OMXKeypadEvent e)
+{
+ int thisKey = e.key();
+ int keyPos = thisKey - 11;
+ int seqKey = keyPos + (sequencer.patternPage[sequencer.playingPattern] * NUM_STEPKEYS);
+
+ uint8_t seqMode = getSequencerMode();
+
+ // Sequencer row keys
+
+ // ### KEY PRESS EVENTS
+
+ if (e.down() && thisKey != 0)
+ {
+ // set key timer to zero
+ // keyPressTime[thisKey] = 0;
+
+ // NOTE SELECT
+ if (seqMode == SEQMODE_NOTESEL)
+ {
+ // SET NOTE
+ // left and right keys change the octave
+ if (thisKey == 11 || thisKey == 26)
+ {
+ int amt = thisKey == 11 ? -1 : 1;
+ midiSettings.octave = constrain(midiSettings.octave + amt, -5, 4);
+ // otherwise select the note
+ }
+ else
+ {
+ if (!e.held()) // Prevent held F1 key from changing note.
+ {
+ // stepSelect_ = false;
+ seqConfig.selectedNote = thisKey;
+
+ uint8_t adjNote = getAdjustedNote(thisKey);
+ // int adjnote = notes[thisKey] + (midiSettings.octave * 12);
+ getSelectedStep()->note = adjNote;
+ if (!sequencer.playing)
+ {
+ seqNoteOn(thisKey, midiSettings.defaultVelocity, sequencer.playingPattern);
+ }
+ }
+ }
+ // see RELEASE events for more
+ omxDisp.setDirty();
+
+ // // noteSelection_
+ // if (seqConfig.noteSelection)
+ // {
+ // // SET NOTE
+ // // left and right keys change the octave
+ // if (thisKey == 11 || thisKey == 26)
+ // {
+ // int amt = thisKey == 11 ? -1 : 1;
+ // midiSettings.newoctave = constrain(midiSettings.octave + amt, -5, 4);
+ // if (midiSettings.newoctave != midiSettings.octave)
+ // {
+ // midiSettings.octave = midiSettings.newoctave;
+ // }
+ // // otherwise select the note
+ // }
+ // else
+ // {
+ // seqConfig.stepSelect = false;
+ // seqConfig.selectedNote = thisKey;
+
+ // uint8_t adjNote = getAdjustedNote(thisKey);
+ // // int adjnote = notes[thisKey] + (midiSettings.octave * 12);
+ // getSelectedStep()->note = adjNote;
+ // if (!sequencer.playing)
+ // {
+ // seqNoteOn(thisKey, midiSettings.defaultVelocity, sequencer.playingPattern);
+ // }
+ // }
+ // // see RELEASE events for more
+ // omxDisp.setDirty();
+ // }
+ // else if (thisKey == 1)
+ // {
+ // }
+ // else if (thisKey == 2)
+ // {
+ // }
+ // else if (thisKey > 2 && thisKey < 11)
+ // { // Pattern select keys
+ // sequencer.playingPattern = thisKey - 3;
+ // omxDisp.setDirty();
+ // }
+ // else if (thisKey > 10)
+ // {
+ // seqConfig.selectedStep = seqKey; // was keyPos // set noteSelection to this step
+ // seqConfig.stepSelect = true;
+ // seqConfig.noteSelection = true;
+ // omxDisp.setDirty();
+ // }
+ }
+ // PATTERN PARAMS
+ else if (seqMode == SEQMODE_PAT)
+ {
+ if (thisKey == 1)
+ { // F1
+ }
+ else if (thisKey == 2)
+ { // F2
+ }
+ else if (thisKey > 2 && thisKey < 11)
+ { // Pattern select keys
+
+ sequencer.playingPattern = thisKey - 3;
+
+ // COPY / PASTE / CLEAR
+ if (midiSettings.keyState[1] && !midiSettings.keyState[2])
+ {
+ copyPattern(sequencer.playingPattern);
+ omxDisp.displayMessagef("COPIED P-%d", sequencer.playingPattern + 1);
+ }
+ else if (!midiSettings.keyState[1] && midiSettings.keyState[2])
+ {
+ pastePattern(sequencer.playingPattern);
+ omxDisp.displayMessagef("PASTED P-%d", sequencer.playingPattern + 1);
+ }
+ else if (midiSettings.keyState[1] && midiSettings.keyState[2])
+ {
+ clearPattern(sequencer.playingPattern);
+ omxDisp.displayMessagef("CLEARED P-%d", sequencer.playingPattern + 1);
+ }
+
+ omxDisp.setDirty();
+ }
+ else if (thisKey > 10)
+ {
+ // set pattern length with key
+ auto newPatternLen = thisKey - 10;
+ sequencer.setPatternLength(sequencer.playingPattern, newPatternLen);
+ if (sequencer.seqPos[sequencer.playingPattern] >= newPatternLen)
+ {
+ sequencer.seqPos[sequencer.playingPattern] = newPatternLen - 1;
+ sequencer.patternPage[sequencer.playingPattern] = getPatternPage(sequencer.seqPos[sequencer.playingPattern]);
+ }
+ omxDisp.setDirty();
+ }
+ }
+ // STEP RECORD
+ else if (seqMode == SEQMODE_STEPRECORD)
+ {
+ seqConfig.selectedNote = thisKey;
+ seqConfig.selectedStep = sequencer.seqPos[sequencer.playingPattern];
+
+ // int adjnote = notes[thisKey] + (midiSettings.octave * 12);
+ uint8_t adjnote = getAdjustedNote(thisKey);
+ getSelectedStep()->note = adjnote;
+
+ if (!sequencer.playing)
+ {
+ seqNoteOn(thisKey, midiSettings.defaultVelocity, sequencer.playingPattern);
+ } // see RELEASE events for more
+ stepDirty_ = true;
+ omxDisp.setDirty();
+ }
+ else if (seqMode == SEQMODE_MAIN)
+ {
+ // MIDI SOLO
+ if (sequencer.getCurrentPattern()->solo)
+ {
+ omxUtil.midiNoteOn(thisKey, midiSettings.defaultVelocity, sequencer.getCurrentPattern()->channel + 1);
+ }
+ // REGULAR SEQ MODE
+ else
+ {
+ if (midiSettings.keyState[1] && midiSettings.keyState[2])
+ {
+ seqPages_ = true;
+ }
+ if (thisKey == 1)
+ {
+ // seqResetFlag = true; // RESET ALL SEQUENCES TO FIRST/LAST STEP
+ // MOVED DOWN TO AUX KEY
+ }
+ else if (thisKey == 2)
+ { // CHANGE PATTERN DIRECTION
+ // sequencer.getCurrentPattern()->reverse = !sequencer.getCurrentPattern()->reverse;
+
+ // BLACK KEYS - PATTERNS
+ }
+ else if (thisKey > 2 && thisKey < 11)
+ { // Pattern select
+
+ // CHECK keyState[] FOR LONG PRESS THINGS
+
+ // If ONLY KEY 1 is down + pattern is not playing = STEP RECORD
+ if (midiSettings.keyState[1] && !midiSettings.keyState[2] && !sequencer.playing)
+ {
+ // ENTER STEP RECORD MODE
+ sequencer.playingPattern = thisKey - 3;
+ sequencer.seqPos[sequencer.playingPattern] = 0;
+ sequencer.patternPage[sequencer.playingPattern] = 0; // Step Record always starts from first page
+
+ changeSequencerMode(SEQMODE_STEPRECORD);
+ // omxDisp.setDirty();;
+ }
+ // If KEY 2 is down + pattern = PATTERN MUTE
+ else if (midiSettings.keyState[2])
+ {
+ if (sequencer.getPattern(thisKey - 3)->mute)
+ {
+ omxDisp.displayMessagef("UNMUTE P-%d", (thisKey - 3) + 1);
+ }
+ else
+ {
+ omxDisp.displayMessagef("MUTE P-%d", (thisKey - 3) + 1);
+ }
+ sequencer.getPattern(thisKey - 3)->mute = !sequencer.getPattern(thisKey - 3)->mute;
+ }
+ else
+ {
+ sequencer.playingPattern = thisKey - 3;
+ }
+ omxDisp.setDirty();
+ }
+ // SEQUENCE 1-16 STEP KEYS
+ else if (thisKey > 10)
+ {
+
+ // F1+F2 HOLD
+ if (midiSettings.keyState[1] && midiSettings.keyState[2])
+ {
+ // IGNORE LONG PRESSES IN STEP RECORD
+ if (!stepRecord_)
+ {
+ if (keyPos <= getPatternPage(sequencer.getCurrentPattern()->len))
+ {
+ sequencer.patternPage[sequencer.playingPattern] = keyPos;
+ }
+ omxDisp.displayMessagef("PATT PAGE %d", keyPos + 1);
+ }
+ }
+ // F1 HOLD
+ else if (midiSettings.keyState[1])
+ {
+ // IGNORE LONG PRESSES IN STEP RECORD and Pattern Params
+ if (!stepRecord_ && !patternParams_)
+ {
+ seqConfig.selectedStep = (thisKey - 11) + (sequencer.patternPage[sequencer.playingPattern] * NUM_STEPKEYS); // set noteSelection to this step
+ // seqConfig.noteSelect = true;
+ // seqConfig.stepSelect = true;
+ // seqConfig.noteSelection = true;
+ // omxDisp.setDirty();
+ // omxDisp.displayMessagef("NOTE SELECT");
+
+ auto selectedStep = getSelectedStep();
+ stepCopyBuffer_.CopyFrom(selectedStep);
+
+ changeSequencerMode(SEQMODE_NOTESEL);
+ // re-toggle the key you just held
+ // if (getSelectedStep()->trig == TRIGTYPE_PLAY || getSelectedStep()->trig == TRIGTYPE_MUTE ) {
+ // getSelectedStep()->trig = (getSelectedStep()->trig == TRIGTYPE_PLAY ) ? TRIGTYPE_MUTE : TRIGTYPE_PLAY;
+ // }
+ }
+ }
+ // F2 HOLD - CUT / PASTE
+ else if (midiSettings.keyState[2])
+ {
+ // paste copied note to current
+ seqConfig.selectedStep = (thisKey - 11) + (sequencer.patternPage[sequencer.playingPattern] * NUM_STEPKEYS); // set noteSelection to this step
+ auto selectedStep = getSelectedStep();
+
+ if (selectedStep->trig == TRIGTYPE_MUTE) // paste copied note to current if trig is off
+ {
+ selectedStep->CopyFrom(&stepCopyBuffer_);
+ tempString = "Paste " + String(seqConfig.selectedStep);
+ omxDisp.displayMessage(tempString.c_str());
+ }
+ else // Cut - copy and turn trig off if trig on
+ {
+ stepCopyBuffer_.CopyFrom(selectedStep);
+ selectedStep->trig = TrigType::TRIGTYPE_MUTE;
+ tempString = "Cut " + String(seqConfig.selectedStep);
+ omxDisp.displayMessage(tempString.c_str());
+ }
+ }
+ else
+ {
+ // TOGGLE STEP ON/OFF
+ if (sequencer.getCurrentPattern()->steps[seqKey].trig == TRIGTYPE_PLAY || sequencer.getCurrentPattern()->steps[seqKey].trig == TRIGTYPE_MUTE)
+ {
+ sequencer.getCurrentPattern()->steps[seqKey].trig = (sequencer.getCurrentPattern()->steps[seqKey].trig == TRIGTYPE_PLAY) ? TRIGTYPE_MUTE : TRIGTYPE_PLAY;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // ### KEY RELEASE EVENTS
+ if (!e.down() && thisKey != 0)
+ {
+ // MIDI SOLO
+ if (sequencer.getCurrentPattern()->solo)
+ {
+ omxUtil.midiNoteOff(thisKey, sequencer.getCurrentPattern()->channel + 1);
+ }
+ }
+
+ if (!e.down() && thisKey != 0 && (noteSelect_ || stepRecord_) && seqConfig.selectedNote > 0)
+ {
+ if (!sequencer.playing)
+ {
+ seqNoteOff(thisKey, sequencer.playingPattern);
+ }
+ if (stepRecord_ && stepDirty_)
+ {
+ step_ahead();
+ stepDirty_ = false;
+
+ seqConfig.selectedStep = sequencer.seqPos[sequencer.playingPattern];
+
+ // EXIT STEP RECORD AFTER THE LAST STEP IN PATTERN
+ if (sequencer.seqPos[sequencer.playingPattern] == 0)
+ {
+ changeSequencerMode(SEQMODE_MAIN);
+ }
+ }
+ }
+
+ // AUX KEY PRESS EVENTS
+
+ if (e.down() && thisKey == 0)
+ {
+ if (seqMode == SEQMODE_NOTESEL)
+ {
+ // if (seqConfig.noteSelection)
+ // {
+ // seqConfig.selectedStep = 0;
+ // seqConfig.selectedNote = 0;
+ // }
+
+ seqConfig.selectedStep = 0;
+ seqConfig.selectedNote = 0;
+
+ changeSequencerMode(SEQMODE_MAIN);
+ }
+ else if (seqMode == SEQMODE_PAT || seqMode == SEQMODE_STEPRECORD)
+ {
+ changeSequencerMode(SEQMODE_MAIN);
+ }
+ else if (seqPages_)
+ {
+ seqPages_ = false;
+ }
+ else
+ {
+ if (midiSettings.keyState[1] || midiSettings.keyState[2])
+ { // CHECK keyState[] FOR LONG PRESS OF FUNC KEYS
+ if (midiSettings.keyState[1])
+ {
+ sequencer.seqResetFlag = true; // RESET ALL SEQUENCES TO FIRST/LAST STEP
+ omxDisp.displayMessagef("RESET");
+ }
+ else if (midiSettings.keyState[2])
+ { // CHANGE PATTERN DIRECTION
+ sequencer.getCurrentPattern()->reverse = !sequencer.getCurrentPattern()->reverse;
+ if (sequencer.getCurrentPattern()->reverse)
+ {
+ omxDisp.displayMessagef("<< REV");
+ }
+ else
+ {
+ omxDisp.displayMessagef("FWD >>");
+ }
+ }
+ omxLeds.setDirty();
+ omxDisp.setDirty();
+ }
+ else
+ {
+ if (sequencer.playing)
+ {
+ // stop transport
+ sequencer.playing = 0;
+ allNotesOff();
+ // Serial.println("stop transport");
+ seqStop();
+ }
+ else
+ {
+ // start transport
+ // Serial.println("start transport");
+ seqStart();
+ }
+ }
+ }
+
+ // AUX KEY RELEASE EVENTS
+ }
+ else if (!e.down() && thisKey == 0)
+ {
+ }
+
+ if (!e.down() && (thisKey == 1 || thisKey == 2))
+ {
+ if (!midiSettings.keyState[1] || !midiSettings.keyState[2])
+ {
+ // Release page selection whenever F1 && F2 are released
+ seqPages_ = false;
+ }
+ }
+
+ // if (!midiSettings.keyState[1] && !midiSettings.keyState[2])
+ // {
+ // seqPageParams.seqPages = false;
+ // }
+
+ // strip.show();
+}
+
+void OmxModeSequencer::onKeyHeldUpdate(OMXKeypadEvent e)
+{
+ int thisKey = e.key();
+
+ if (!sequencer.getCurrentPattern()->solo)
+ {
+ // TODO: access key state directly in omx_keypad.h
+ if (midiSettings.keyState[1] && midiSettings.keyState[2])
+ {
+ seqPages_ = true;
+ }
+ // SKIP LONG PRESS IF FUNC KEYS ARE ALREDY HELD
+ else if (!midiSettings.keyState[1] && !midiSettings.keyState[2])
+ {
+ // If in main mode
+ if (getSequencerMode() == SEQMODE_MAIN)
+ {
+ // skip AUX key, get pattern keys
+ if (thisKey > 2 && thisKey < 11)
+ {
+ if (!stepRecord_)
+ {
+ changeSequencerMode(SEQMODE_PAT);
+ }
+ }
+ else if (thisKey > 10)
+ {
+ // IGNORE LONG PRESSES IN STEP RECORD and Pattern Params
+ seqConfig.selectedStep = (thisKey - 11) + (sequencer.patternPage[sequencer.playingPattern] * NUM_STEPKEYS); // set noteSelection to this step
+ // seqConfig.noteSelect = true;
+ // seqConfig.stepSelect = true;
+ // seqConfig.noteSelection = true;
+ // omxDisp.setDirty();
+ // omxDisp.displayMessagef("NOTE SELECT");
+
+ // Copy the step to the buffer
+ auto selectedStep = getSelectedStep();
+ stepCopyBuffer_.CopyFrom(selectedStep);
+
+ changeSequencerMode(SEQMODE_NOTESEL);
+ // re-toggle the key you just held
+ // if ( getSelectedStep()->trig == TRIGTYPE_PLAY || getSelectedStep()->trig == TRIGTYPE_MUTE ) {
+ // getSelectedStep()->trig = ( getSelectedStep()->trig == TRIGTYPE_PLAY ) ? TRIGTYPE_MUTE : TRIGTYPE_PLAY;
+ // }
+ }
+ }
+ }
+ }
+}
+
+void OmxModeSequencer::showCurrentStepLEDs(int patternNum)
+{
+ // omxLeds.updateBlinkStates();
+
+ if (sysSettings.screenSaverMode && !sequencer.playing)
+ return; // Screensaver active and not playing, don't update sequencer LEDs.
+
+ bool blinkState = omxLeds.getBlinkState();
+ bool slowBlinkState = omxLeds.getSlowBlinkState();
+
+ // AUX KEY
+
+ if (sequencer.playing && blinkState)
+ {
+ strip.setPixelColor(0, WHITE);
+ }
+ else if (noteSelect_ && blinkState)
+ {
+ strip.setPixelColor(0, NOTESEL);
+ }
+ else if (patternParams_ && blinkState)
+ {
+ strip.setPixelColor(0, seqColors[patternNum]);
+ }
+ else if (stepRecord_ && blinkState)
+ {
+ strip.setPixelColor(0, seqColors[patternNum]);
+ }
+ else
+ {
+ if (!seq2Mode) // S1
+ {
+ strip.setPixelColor(0, SEQ1C);
+ }
+ else
+ { // S2
+ strip.setPixelColor(0, SEQ2C);
+ }
+
+ // Default was strip.setPixelColor(0, LEDOFF); should never happen
+ }
+
+ if (sequencer.getPattern(patternNum)->mute)
+ {
+ colorConfig.stepColor = muteColors[patternNum];
+ }
+ else
+ {
+ colorConfig.stepColor = seqColors[patternNum];
+ colorConfig.muteColor = muteColors[patternNum];
+ }
+
+ auto currentpage = sequencer.patternPage[patternNum];
+ auto pagestepstart = (currentpage * NUM_STEPKEYS);
+
+ uint8_t seqMode = getSequencerMode();
+
+ // NOTE SELECTION
+ if (seqMode == SEQMODE_NOTESEL)
+ {
+ uint8_t seqPos = seqConfig.selectedStep;
+ uint8_t currentNote = sequencer.patterns[sequencer.playingPattern].steps[seqPos].note;
+
+ // 27 LEDS so use LED_COUNT
+ for (int j = 1; j < LED_COUNT; j++)
+ {
+ auto pixelpos = j;
+ auto selectedStepPixel = (seqConfig.selectedStep % NUM_STEPKEYS) + 11;
+ auto adjNote = getAdjustedNote(j);
+
+ if (adjNote == currentNote)
+ {
+ strip.setPixelColor(pixelpos, HALFWHITE);
+ }
+ else if (pixelpos == selectedStepPixel)
+ {
+ strip.setPixelColor(pixelpos, SEQSTEP);
+ }
+ else
+ {
+ strip.setPixelColor(pixelpos, LEDOFF);
+ }
+
+ // Blink left/right keys for octave select indicators.
+ auto color1 = blinkState ? ORANGE : WHITE;
+ auto color2 = blinkState ? RBLUE : WHITE;
+ strip.setPixelColor(11, color1);
+ strip.setPixelColor(26, color2);
+ }
+ }
+ // STEP RECORD
+ else if (seqMode == SEQMODE_STEPRECORD)
+ {
+ uint8_t seqPos = sequencer.seqPos[sequencer.playingPattern];
+ uint8_t currentNote = sequencer.patterns[sequencer.playingPattern].steps[seqPos].note;
+
+ int seqPosNoteColor = LEDOFF;
+
+ // 27 LEDS so use LED_COUNT
+ // This loop sets the key matching the current note to be on and turns other leds off.
+ for (int j = 1; j < LED_COUNT; j++)
+ {
+ auto pixelpos = j;
+ auto adjNote = getAdjustedNote(j);
+
+ // Serial.println((String)"seqPos: " + seqPos + " currentNote: " + currentNote + " pixelPos: " + pixelpos + " adjNote: " + adjNote);
+
+ if (adjNote == currentNote)
+ {
+ strip.setPixelColor(pixelpos, HALFWHITE);
+
+ // will be overwritten by step indicator
+ if (j - 11 == seqPos % 16)
+ {
+ seqPosNoteColor = HALFWHITE;
+ }
+ }
+ else
+ {
+ strip.setPixelColor(pixelpos, LEDOFF);
+ }
+ }
+
+ for (int j = pagestepstart; j < (pagestepstart + NUM_STEPKEYS); j++)
+ {
+ auto pixelpos = j - pagestepstart + 11;
+ // ONLY DO LEDS FOR THE CURRENT PAGE
+ if (j == seqPos)
+ {
+ // Blinks with the current note number if overlapped, blinks with LEDOFF otherwise.
+ strip.setPixelColor(pixelpos, slowBlinkState ? SEQCHASE : seqPosNoteColor);
+ }
+ }
+ }
+ else if (sequencer.getCurrentPattern()->solo)
+ { // MIDI SOLO
+
+ // for(int i = 0; i < NUM_STEPKEYS; i++){
+ // if (i == seqPos[patternNum]){
+ // if (playing){
+ // strip.setPixelColor(i+11, SEQCHASE); // step chase
+ // } else {
+ // strip.setPixelColor(i+11, LEDOFF); // DO WE NEED TO MARK PLAYHEAD WHEN STOPPED?
+ // }
+ // } else {
+ // strip.setPixelColor(i+11, LEDOFF);
+ // }
+ // }
+ }
+ else if (seqPages_)
+ {
+ // BLINK F1+F2
+ auto color1 = blinkState ? FUNKONE : LEDOFF;
+ auto color2 = blinkState ? FUNKTWO : LEDOFF;
+ strip.setPixelColor(1, color1);
+ strip.setPixelColor(2, color2);
+
+ // TURN OFF LEDS
+ // 27 LEDS so use LED_COUNT
+ for (int j = 3; j < LED_COUNT; j++)
+ { // START WITH LEDS AFTER F-KEYS
+ strip.setPixelColor(j, LEDOFF);
+ }
+ // SHOW LEDS FOR WHAT PAGE OF SEQ PATTERN YOURE ON
+ auto len = (sequencer.getPattern(patternNum)->len / NUM_STEPKEYS);
+ for (int h = 0; h <= len; h++)
+ {
+ auto currentpage = sequencer.patternPage[patternNum];
+ auto color = sequencePageColors[h];
+ if (h == currentpage)
+ {
+ color = blinkState ? sequencePageColors[currentpage] : LEDOFF;
+ }
+ strip.setPixelColor(11 + h, color);
+ }
+ }
+ // PATTERN or MAIN
+ else
+ {
+ for (int j = 1; j < LED_COUNT; j++)
+ {
+ if (j < sequencer.getPatternLength(patternNum) + 11)
+ {
+ if (j == 1)
+ {
+ // NOTE SELECT / F1
+ if (midiSettings.keyState[j] && blinkState)
+ {
+ strip.setPixelColor(j, LEDOFF);
+ }
+ else
+ {
+ strip.setPixelColor(j, FUNKONE);
+ }
+ }
+ else if (j == 2)
+ {
+ // PATTERN PARAMS / F2
+ if (midiSettings.keyState[j] && blinkState)
+ {
+ strip.setPixelColor(j, LEDOFF);
+ }
+ else
+ {
+ strip.setPixelColor(j, FUNKTWO);
+ }
+ }
+ else if (j == patternNum + 3)
+ { // PATTERN SELECT
+ strip.setPixelColor(j, colorConfig.stepColor);
+ if (patternParams_ && blinkState)
+ {
+ strip.setPixelColor(j, LEDOFF);
+ }
+ }
+ else
+ {
+ strip.setPixelColor(j, LEDOFF);
+ }
+ }
+ else
+ {
+ strip.setPixelColor(j, LEDOFF);
+ }
+ }
+
+ auto pattern = sequencer.getPattern(patternNum);
+ auto steps = pattern->steps;
+ auto currentpage = sequencer.patternPage[patternNum];
+ auto pagestepstart = (currentpage * NUM_STEPKEYS);
+
+ // WHAT TO DO HERE FOR MULTIPLE PAGES
+ // NUM_STEPKEYS or NUM_STEPS INSTEAD?
+ for (int i = pagestepstart; i < (pagestepstart + NUM_STEPKEYS); i++)
+ {
+ if (i < sequencer.getPatternLength(patternNum))
+ {
+
+ // ONLY DO LEDS FOR THE CURRENT PAGE
+ auto pixelpos = i - pagestepstart + 11;
+ // if (patternParams){
+ // strip.setPixelColor(pixelpos, SEQMARKER);
+ // }
+
+ if (i % 4 == 0)
+ { // MARK GROUPS OF 4
+ if (i == sequencer.lastSeqPos[patternNum])
+ {
+ if (sequencer.playing)
+ {
+ strip.setPixelColor(pixelpos, SEQCHASE); // step chase
+ }
+ else if (steps[i].trig == TRIGTYPE_PLAY)
+ {
+ if (steps[i].stepType != STEPTYPE_NONE)
+ {
+ if (slowBlinkState)
+ {
+ strip.setPixelColor(pixelpos, colorConfig.stepColor); // STEP EVENT COLOR
+ }
+ else
+ {
+ strip.setPixelColor(pixelpos, colorConfig.muteColor); // STEP EVENT COLOR
+ }
+ }
+ else
+ {
+ strip.setPixelColor(pixelpos, colorConfig.stepColor); // STEP ON COLOR
+ }
+ }
+ else if (steps[i].trig == TRIGTYPE_MUTE)
+ {
+ strip.setPixelColor(pixelpos, SEQMARKER);
+ }
+ }
+ else if (steps[i].trig == TRIGTYPE_PLAY)
+ {
+ if (steps[i].stepType != STEPTYPE_NONE)
+ {
+ if (slowBlinkState)
+ {
+ strip.setPixelColor(pixelpos, colorConfig.stepColor); // STEP EVENT COLOR
+ }
+ else
+ {
+ strip.setPixelColor(pixelpos, colorConfig.muteColor); // STEP EVENT COLOR
+ }
+ }
+ else
+ {
+ strip.setPixelColor(pixelpos, colorConfig.stepColor); // STEP ON COLOR
+ }
+ }
+ else if (steps[i].trig == TRIGTYPE_MUTE)
+ {
+ strip.setPixelColor(pixelpos, SEQMARKER);
+ }
+ }
+ else if (i == sequencer.lastSeqPos[patternNum])
+ { // STEP CHASE
+ if (sequencer.playing)
+ {
+ strip.setPixelColor(pixelpos, SEQCHASE);
+ }
+ else if (steps[i].trig == TRIGTYPE_PLAY)
+ {
+ if (steps[i].stepType != STEPTYPE_NONE)
+ {
+ if (slowBlinkState)
+ {
+ strip.setPixelColor(pixelpos, colorConfig.stepColor); // STEP EVENT COLOR
+ }
+ else
+ {
+ strip.setPixelColor(pixelpos, colorConfig.muteColor); // STEP EVENT COLOR
+ }
+ }
+ else
+ {
+ strip.setPixelColor(pixelpos, colorConfig.stepColor); // STEP ON COLOR
+ }
+ }
+ else if (!patternParams_ && sequencer.patterns[patternNum].steps[i].trig == TRIGTYPE_MUTE)
+ {
+ strip.setPixelColor(pixelpos, LEDOFF); // DO WE NEED TO MARK PLAYHEAD WHEN STOPPED?
+ }
+ else if (patternParams_)
+ {
+ strip.setPixelColor(pixelpos, SEQMARKER);
+ }
+ }
+ else if (steps[i].trig == TRIGTYPE_PLAY)
+ {
+ if (steps[i].stepType != STEPTYPE_NONE)
+ {
+ if (slowBlinkState)
+ {
+ strip.setPixelColor(pixelpos, colorConfig.stepColor); // STEP EVENT COLOR
+ }
+ else
+ {
+ strip.setPixelColor(pixelpos, colorConfig.muteColor); // STEP EVENT COLOR
+ }
+ }
+ else
+ {
+ strip.setPixelColor(pixelpos, colorConfig.stepColor); // STEP ON COLOR
+ }
+ }
+ else if (!patternParams_ && steps[i].trig == TRIGTYPE_MUTE)
+ {
+ strip.setPixelColor(pixelpos, LEDOFF);
+ }
+ else if (patternParams_)
+ {
+ strip.setPixelColor(pixelpos, SEQMARKER);
+ }
+ }
+ }
+ }
+ omxLeds.setDirty();
+}
+
+void OmxModeSequencer::updateLEDs()
+{
+ showCurrentStepLEDs(sequencer.playingPattern);
+}
+
+void OmxModeSequencer::onDisplayUpdate()
+{
+ // MIDI SOLO
+ if (sequencer.getCurrentPattern()->solo)
+ {
+ omxLeds.drawMidiLeds(musicScale);
+ }
+ // DISPLAY
+ if (omxDisp.isDirty())
+ {
+ // show only if not encoder edit or dialog display
+ if (!encoderConfig.enc_edit && omxDisp.isMessageActive() == false)
+ {
+ uint8_t seqMode = getSequencerMode();
+ if (seqMode == SEQMODE_MAIN)
+ {
+ if (seqParams.getSelPage() == 0) // SUBMODE_SEQ
+ {
+ omxDisp.clearLegends();
+ omxDisp.legends[0] = "PTN";
+ omxDisp.legends[1] = "TRSP";
+ omxDisp.legends[2] = "SWNG"; //"TRSP";
+ omxDisp.legends[3] = "BPM";
+ omxDisp.legendVals[0] = sequencer.playingPattern + 1;
+ omxDisp.legendVals[1] = (int)midiSettings.transpose;
+ omxDisp.legendVals[2] = (int)sequencer.getCurrentPattern()->swing; //(int)swing;
+ // legendVals[2] = swing_values[sequencer.getCurrentPattern()->swing];
+ omxDisp.legendVals[3] = (int)clockConfig.clockbpm;
+ }
+ else if (seqParams.getSelPage() == 1) // SUBMODE_SEQ2
+ {
+ omxDisp.clearLegends();
+ omxDisp.legends[0] = "SOLO";
+ omxDisp.legends[1] = "LEN";
+ omxDisp.legends[2] = "RATE";
+ omxDisp.legends[3] = "CV"; // cvPattern
+ omxDisp.legendVals[0] = sequencer.getCurrentPattern()->solo; // playingPattern+1;
+ omxDisp.legendVals[1] = sequencer.getPatternLength(sequencer.playingPattern);
+ omxDisp.legendVals[2] = -127;
+ omxDisp.legendText[2] = mdivs[sequencer.getCurrentPattern()->clockDivMultP];
+ omxDisp.legendVals[3] = -127; // TODO is this right?
+ if (sequencer.getCurrentPattern()->sendCV)
+ {
+ omxDisp.legendText[3] = "On";
+ }
+ else
+ {
+ omxDisp.legendText[3] = "Off";
+ }
+ }
+ omxDisp.dispGenericMode2(2, seqParams.getSelPage(), seqParams.getSelParam(), encoderSelect_);
+ }
+ else if (seqMode == SEQMODE_NOTESEL)
+ {
+ if (noteSelParams.getSelPage() == 0) // SUBMODE_NOTESEL
+ {
+ omxDisp.clearLegends();
+ omxDisp.legends[0] = "NOTE";
+ omxDisp.legends[1] = "OCT";
+ omxDisp.legends[2] = "VEL";
+ omxDisp.legends[3] = "LEN";
+ omxDisp.legendVals[0] = getSelectedStep()->note;
+ omxDisp.legendVals[1] = (int)midiSettings.octave + 4;
+ omxDisp.legendVals[2] = getSelectedStep()->vel;
+ omxDisp.useLegendString[3] = true;
+ omxDisp.legendString[3] = String(kNoteLengths[getSelectedStep()->len]);
+ }
+ else if (noteSelParams.getSelPage() == 1) // SUBMODE_NOTESEL2
+ {
+ omxDisp.clearLegends();
+ omxDisp.legends[0] = "TYPE";
+ omxDisp.legends[1] = "PROB";
+ omxDisp.legends[2] = "COND";
+ omxDisp.legends[3] = "";
+ omxDisp.legendVals[0] = -127;
+ omxDisp.legendText[0] = stepTypes[getSelectedStep()->stepType];
+ omxDisp.legendVals[1] = getSelectedStep()->prob;
+ // String ac = String(trigConditionsAB[][0]);
+ // String bc = String(trigConditionsAB[getSelectedStep()->condition][1]);
+
+ omxDisp.legendVals[2] = -127;
+ omxDisp.legendText[2] = trigConditions[getSelectedStep()->condition]; // ac + bc; // trigConditions
+
+ omxDisp.legendVals[3] = 0;
+ }
+ else if (noteSelParams.getSelPage() == 2) // SUBMODE_NOTESEL3
+ {
+ omxDisp.clearLegends();
+ omxDisp.legends[0] = "L-1";
+ omxDisp.legends[1] = "L-2";
+ omxDisp.legends[2] = "L-3";
+ omxDisp.legends[3] = "L-4";
+ for (int j = 0; j < 4; j++)
+ {
+ int stepNoteParam = getSelectedStep()->params[j];
+ if (stepNoteParam > -1)
+ {
+ omxDisp.legendVals[j] = stepNoteParam;
+ }
+ else
+ {
+ omxDisp.legendVals[j] = -127;
+ omxDisp.legendText[j] = "---";
+ }
+ }
+ }
+ omxDisp.dispGenericMode2(3, noteSelParams.getSelPage(), noteSelParams.getSelParam(), encoderSelect_);
+ }
+ else if (seqMode == SEQMODE_PAT)
+ {
+ if (patParams.getSelPage() == 0) // SUBMODE_PATTPARAMS
+ {
+ omxDisp.clearLegends();
+ omxDisp.legends[0] = "PTN";
+ omxDisp.legends[1] = "LEN";
+ omxDisp.legends[2] = "ROT";
+ omxDisp.legends[3] = "CHAN";
+ omxDisp.legendVals[0] = sequencer.playingPattern + 1;
+ omxDisp.legendVals[1] = sequencer.getPatternLength(sequencer.playingPattern);
+ omxDisp.legendVals[2] = midiSettings.rotationAmt; //(int)transpose;
+ omxDisp.legendVals[3] = sequencer.getPatternChannel(sequencer.playingPattern);
+ }
+ else if (patParams.getSelPage() == 1) // SUBMODE_PATTPARAMS2
+ {
+ omxDisp.clearLegends();
+ omxDisp.legends[0] = "START";
+ omxDisp.legends[1] = "END";
+ omxDisp.legends[2] = "FREQ";
+ omxDisp.legends[3] = "PROB";
+ omxDisp.legendVals[0] = sequencer.getCurrentPattern()->startstep + 1; // STRT step to autoreset on
+ omxDisp.legendVals[1] = sequencer.getCurrentPattern()->autoresetstep; // STP step to autoreset on - 0 = no auto reset
+ omxDisp.legendVals[2] = sequencer.getCurrentPattern()->autoresetfreq; // FRQ to autoreset on -- every x cycles
+ omxDisp.legendVals[3] = sequencer.getCurrentPattern()->autoresetprob; // PRO probability of resetting 0=NEVER 1=Always 2=50%
+ }
+ else if (patParams.getSelPage() == 2) // SUBMODE_PATTPARAMS3
+ {
+ omxDisp.clearLegends();
+ omxDisp.legends[0] = "RATE";
+ omxDisp.legends[1] = "SOLO";
+ omxDisp.legends[2] = "---";
+ omxDisp.legends[3] = "---";
+
+ // RATE FOR CURR PATTERN
+ omxDisp.legendVals[0] = -127;
+ omxDisp.legendText[0] = mdivs[sequencer.getCurrentPattern()->clockDivMultP];
+
+ omxDisp.legendVals[1] = sequencer.getCurrentPattern()->solo;
+ omxDisp.legendVals[2] = 0; // TBD
+ omxDisp.legendVals[3] = 0; // TBD
+ }
+ omxDisp.dispGenericMode2(3, patParams.getSelPage(), patParams.getSelParam(), encoderSelect_);
+ }
+ else if (seqMode == SEQMODE_STEPRECORD)
+ {
+ if (sRecParams.getSelPage() == 0) // SUBMODE_STEPREC
+ {
+ omxDisp.clearLegends();
+ omxDisp.legends[0] = "OCT";
+ omxDisp.legends[1] = "STEP";
+ omxDisp.legends[2] = "NOTE";
+ omxDisp.legends[3] = "PTN";
+ omxDisp.legendVals[0] = (int)midiSettings.octave + 4;
+ omxDisp.legendVals[1] = sequencer.seqPos[sequencer.playingPattern] + 1;
+ omxDisp.legendVals[2] = getSelectedStep()->note; //(int)transpose;
+ omxDisp.legendVals[3] = sequencer.playingPattern + 1;
+ }
+ else if (sRecParams.getSelPage() == 1) // SUBMODE_NOTESEL2
+ {
+ omxDisp.clearLegends();
+ omxDisp.legends[0] = "TYPE";
+ omxDisp.legends[1] = "PROB";
+ omxDisp.legends[2] = "COND";
+ omxDisp.legends[3] = "";
+ omxDisp.legendVals[0] = -127;
+ omxDisp.legendText[0] = stepTypes[getSelectedStep()->stepType];
+ omxDisp.legendVals[1] = getSelectedStep()->prob;
+ // String ac = String(trigConditionsAB[][0]);
+ // String bc = String(trigConditionsAB[getSelectedStep()->condition][1]);
+
+ omxDisp.legendVals[2] = -127;
+ omxDisp.legendText[2] = trigConditions[getSelectedStep()->condition]; // ac + bc; // trigConditions
+
+ omxDisp.legendVals[3] = 0;
+ }
+ omxDisp.dispGenericMode2(3, sRecParams.getSelPage(), sRecParams.getSelParam(), encoderSelect_);
+ }
+ }
+ }
+}
+
+void OmxModeSequencer::initPatterns()
+{
+ // default to GM Drum Map for now -- GET THIS FROM patternDefaultNoteMap instead
+ // uint8_t initNotes[NUM_PATTERNS] = {
+ // 36,
+ // 38,
+ // 37,
+ // 39,
+ // 42,
+ // 46,
+ // 49,
+ // 51 };
+
+ StepNote stepNote = {0, 100, defaultNoteLength, TRIGTYPE_MUTE, {-1, -1, -1, -1, -1}, 100, 0, STEPTYPE_NONE};
+ // {note, vel, len, TRIGTYPE, {params0, params1, params2, params3, params4}, prob, condition, STEPTYPE}
+
+ for (int i = 0; i < NUM_SEQ_PATTERNS; i++)
+ {
+ auto pattern = sequencer.getPattern(i);
+
+ stepNote.note = sequencer.patternDefaultNoteMap[i]; // Defined in sequencer.h
+
+ for (int j = 0; j < NUM_STEPS; j++)
+ {
+ memcpy(&pattern->steps[j], &stepNote, sizeof(StepNote));
+ }
+
+ // TODO: move to sequencer.h
+ pattern->len = 15;
+ pattern->channel = i; // 0 - 15 becomes 1 - 16
+ pattern->startstep = 0;
+ pattern->autoresetstep = 0;
+ pattern->autoresetfreq = 0;
+ pattern->current_cycle = 1;
+ pattern->rndstep = 3;
+ pattern->clockDivMultP = 2;
+ pattern->autoresetprob = 0;
+ pattern->swing = 0;
+ pattern->reverse = false;
+ pattern->mute = false;
+ pattern->autoreset = false;
+ pattern->solo = false;
+ pattern->sendCV = false;
+ }
+}
+
+void OmxModeSequencer::SetScale(MusicScales *scale)
+{
+ this->musicScale = scale;
+}
diff --git a/Archive/OMX-27-firmware/src/modes/omx_mode_sequencer.h b/Archive/OMX-27-firmware/src/modes/omx_mode_sequencer.h
new file mode 100644
index 00000000..3b08e93f
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/modes/omx_mode_sequencer.h
@@ -0,0 +1,93 @@
+#pragma once
+
+#include "omx_mode_interface.h"
+#include "../utils/music_scales.h"
+#include "../utils/param_manager.h"
+class OmxModeSequencer : public OmxModeInterface
+{
+public:
+ OmxModeSequencer();
+ ~OmxModeSequencer() {}
+
+ void InitSetup() override;
+
+ void initPatterns(); // Initializes all patterns
+
+ void onModeActivated() override;
+
+ void onPotChanged(int potIndex, int prevValue, int newValue, int analogDelta) override;
+
+ void loopUpdate(Micros elapsedTime) override;
+
+ // Should be part of LED update, intertangled with the sequencer class which is calling it in main FW code.
+ void showCurrentStepLEDs(int patternNum);
+
+ void updateLEDs() override;
+
+ void onEncoderChanged(Encoder::Update enc) override;
+ void onEncoderButtonDown() override;
+ void onEncoderButtonDownLong() override;
+
+ bool shouldBlockEncEdit() override;
+
+ void onKeyUpdate(OMXKeypadEvent e) override;
+ void onKeyHeldUpdate(OMXKeypadEvent e) override;
+
+ void onDisplayUpdate() override;
+
+ void setSeq1Mode()
+ {
+ seq2Mode = false;
+ }
+
+ void setSeq2Mode()
+ {
+ seq2Mode = true;
+ }
+
+ void SetScale(MusicScales *scale);
+
+private:
+ bool initSetup = false;
+ bool seq2Mode = false;
+
+ MusicScales *musicScale;
+
+ // These do not appear to be used
+ // bool copiedFlag = false;
+ // bool pastedFlag = false;
+ // bool clearedFlag = false;
+
+ // If true, encoder selects param rather than modifies value
+ bool encoderSelect_ = false;
+
+ bool patternParams_ = false;
+ bool seqPages_ = false; // True when we can change page selection
+
+ bool noteSelect_ = false;
+ // bool noteSelection_ = false; // noteSelection_ is never set false when in noteSelect_ mode, so see no reason for it. seems to be remnant of some other feature.
+
+ // bool stepSelect_ = false; // Only used in noteSelection after selecting a key, it is set false, value never checked, see no reason for it.
+ bool stepRecord_ = false;
+ bool stepDirty_ = false;
+
+ ParamManager seqParams; // seq params, 2 pages
+ ParamManager noteSelParams; // note select params, 3 pages
+ ParamManager patParams; // pattern params, 3 pages
+ ParamManager sRecParams; // step record params, 2 pages
+
+ // void setParam(uint8_t pageIndex, uint8_t paramPosition);
+ // void setParam(uint8_t paramIndex);
+
+ void onEncoderChangedNorm(Encoder::Update enc);
+ void onEncoderChangedStep(Encoder::Update enc);
+
+ void onEncoderChangedSelectParam(Encoder::Update enc);
+
+ uint8_t getAdjustedNote(uint8_t keyNumber);
+
+ void changeSequencerMode(uint8_t newMode);
+ uint8_t getSequencerMode(); // based on enum SequencerMode in cpp file
+
+ void pasteStep(uint8_t stepKey);
+};
diff --git a/Archive/OMX-27-firmware/src/modes/omx_screensaver.cpp b/Archive/OMX-27-firmware/src/modes/omx_screensaver.cpp
new file mode 100644
index 00000000..f3cf5e19
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/modes/omx_screensaver.cpp
@@ -0,0 +1,145 @@
+#include "omx_screensaver.h"
+#include "../consts/consts.h"
+#include "../config.h"
+#include "../utils/omx_util.h"
+#include "../hardware/omx_disp.h"
+#include "../hardware/omx_leds.h"
+
+void OmxScreensaver::setScreenSaverColor()
+{
+ colorConfig.screensaverColor = map(potSettings.analog[4]->getValue(), potMinVal, potMaxVal, 0, ssMaxColorDepth);
+}
+
+void OmxScreensaver::onPotChanged(int potIndex, int prevValue, int newValue, int analogDelta)
+{
+ // set screensaver color with pot 4
+ if (potSettings.analog[4]->hasChanged())
+ {
+ setScreenSaverColor();
+ }
+ // reset screensaver
+ if (potSettings.analog[0]->hasChanged() || potSettings.analog[1]->hasChanged() || potSettings.analog[2]->hasChanged() || potSettings.analog[3]->hasChanged())
+ {
+ screenSaverCounter = 0;
+ }
+}
+
+void OmxScreensaver::updateScreenSaverState()
+{
+ if (screenSaverCounter > screensaverInterval)
+ {
+ if (!screenSaverActive)
+ {
+ screenSaverActive = true;
+ setScreenSaverColor();
+ }
+ }
+ else if (screenSaverCounter < 10)
+ {
+ ssstep = 0;
+ ssloop = 0;
+ // setAllLEDS(0,0,0);
+ screenSaverActive = false;
+ nextStepTimeSS = millis();
+ }
+ else
+ {
+ screenSaverActive = false;
+ nextStepTimeSS = millis();
+ }
+}
+
+bool OmxScreensaver::shouldShowScreenSaver()
+{
+ return screenSaverActive;
+}
+
+void OmxScreensaver::onEncoderChanged(Encoder::Update enc)
+{
+}
+
+void OmxScreensaver::onKeyUpdate(OMXKeypadEvent e)
+{
+}
+
+void OmxScreensaver::onDisplayUpdate()
+{
+ updateLEDs();
+ omxDisp.clearDisplay();
+}
+
+void OmxScreensaver::resetCounter()
+{
+ screenSaverCounter = 0;
+}
+
+void OmxScreensaver::updateLEDs()
+{
+ unsigned long playstepmillis = millis();
+ if (playstepmillis > nextStepTimeSS)
+ {
+ ssstep = ssstep % 16;
+ ssloop = ssloop % 16;
+
+ int j = 26 - ssloop;
+ int i = ssstep + 11;
+
+ for (int z = 1; z < 11; z++)
+ {
+ strip.setPixelColor(z, 0);
+ }
+ if (colorConfig.screensaverColor < ssMaxColorDepth)
+ {
+ if (!ssreverse)
+ {
+ // turn off all leds
+ for (int x = 0; x < 16; x++)
+ {
+ if (i < j)
+ {
+ strip.setPixelColor(x + 11, 0);
+ }
+ if (x + 11 > j)
+ {
+ strip.setPixelColor(x + 11, strip.gamma32(strip.ColorHSV(colorConfig.screensaverColor)));
+ }
+ }
+ strip.setPixelColor(i + 1, strip.gamma32(strip.ColorHSV(colorConfig.screensaverColor)));
+ }
+ else
+ {
+ for (int y = 0; y < 16; y++)
+ {
+ if (i >= j)
+ {
+ strip.setPixelColor(y + 11, 0);
+ }
+ if (y + 11 < j)
+ {
+ strip.setPixelColor(y + 11, strip.gamma32(strip.ColorHSV(colorConfig.screensaverColor)));
+ }
+ }
+ strip.setPixelColor(i + 1, strip.gamma32(strip.ColorHSV(colorConfig.screensaverColor)));
+ }
+ }
+ else
+ {
+ for (int w = 0; w < 27; w++)
+ {
+ strip.setPixelColor(w, 0);
+ }
+ }
+ ssstep++;
+ if (ssstep == 16)
+ {
+ ssloop++;
+ }
+ if (ssloop == 16)
+ {
+ ssreverse = !ssreverse;
+ }
+ nextStepTimeSS = nextStepTimeSS + sleepTick;
+
+ omxLeds.setDirty();
+ }
+}
diff --git a/Archive/OMX-27-firmware/src/modes/omx_screensaver.h b/Archive/OMX-27-firmware/src/modes/omx_screensaver.h
new file mode 100644
index 00000000..b7abd7b3
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/modes/omx_screensaver.h
@@ -0,0 +1,44 @@
+#pragma once
+
+#include "./omx_mode_interface.h"
+
+class OmxScreensaver : public OmxModeInterface
+{
+public:
+ OmxScreensaver() {}
+ ~OmxScreensaver() {}
+
+ void onPotChanged(int potIndex, int prevValue, int newValue, int analogDelta) override;
+
+ void updateLEDs() override;
+
+ void resetCounter();
+
+ void updateScreenSaverState();
+ bool shouldShowScreenSaver();
+
+ void onEncoderChanged(Encoder::Update enc) override;
+
+ void onEncoderButtonDown() override{};
+ void onEncoderButtonDownLong() override{};
+
+ void onKeyUpdate(OMXKeypadEvent e) override;
+ void onKeyHeldUpdate(OMXKeypadEvent e){};
+
+ void onDisplayUpdate() override;
+
+private:
+ void setScreenSaverColor();
+ elapsedMillis screenSaverCounter = 0;
+ unsigned long screensaverInterval = 1000 * 60 * 3; // 3 minutes default
+ uint32_t ssMaxColorDepth = 65528; // used by setScreenSaverColor(). Allows for full rainbow of colors, plus a little extra for 'black'
+
+ int ssstep = 0;
+ int ssloop = 0;
+ volatile unsigned long nextStepTimeSS = 0;
+ bool ssreverse = false;
+
+ int sleepTick = 80;
+
+ bool screenSaverActive;
+};
diff --git a/Archive/OMX-27-firmware/src/modes/retro_grids.cpp b/Archive/OMX-27-firmware/src/modes/retro_grids.cpp
new file mode 100644
index 00000000..d2db09f5
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/modes/retro_grids.cpp
@@ -0,0 +1,825 @@
+#include "retro_grids.h"
+#include "../midi/midi.h"
+
+namespace grids
+{
+ const uint8_t node_0[] = {
+ 255, 0, 0, 0, 0, 0, 145, 0,
+ 0, 0, 0, 0, 218, 0, 0, 0,
+ 72, 0, 36, 0, 182, 0, 0, 0,
+ 109, 0, 0, 0, 72, 0, 0, 0,
+ 36, 0, 109, 0, 0, 0, 8, 0,
+ 255, 0, 0, 0, 0, 0, 72, 0,
+ 0, 0, 182, 0, 0, 0, 36, 0,
+ 218, 0, 0, 0, 145, 0, 0, 0,
+ 170, 0, 113, 0, 255, 0, 56, 0,
+ 170, 0, 141, 0, 198, 0, 56, 0,
+ 170, 0, 113, 0, 226, 0, 28, 0,
+ 170, 0, 113, 0, 198, 0, 85, 0,
+ };
+ const uint8_t node_1[] = {
+ 229, 0, 25, 0, 102, 0, 25, 0,
+ 204, 0, 25, 0, 76, 0, 8, 0,
+ 255, 0, 8, 0, 51, 0, 25, 0,
+ 178, 0, 25, 0, 153, 0, 127, 0,
+ 28, 0, 198, 0, 56, 0, 56, 0,
+ 226, 0, 28, 0, 141, 0, 28, 0,
+ 28, 0, 170, 0, 28, 0, 28, 0,
+ 255, 0, 113, 0, 85, 0, 85, 0,
+ 159, 0, 159, 0, 255, 0, 63, 0,
+ 159, 0, 159, 0, 191, 0, 31, 0,
+ 159, 0, 127, 0, 255, 0, 31, 0,
+ 159, 0, 127, 0, 223, 0, 95, 0,
+ };
+ const uint8_t node_2[] = {
+ 255, 0, 0, 0, 127, 0, 0, 0,
+ 0, 0, 102, 0, 0, 0, 229, 0,
+ 0, 0, 178, 0, 204, 0, 0, 0,
+ 76, 0, 51, 0, 153, 0, 25, 0,
+ 0, 0, 127, 0, 0, 0, 0, 0,
+ 255, 0, 191, 0, 31, 0, 63, 0,
+ 0, 0, 95, 0, 0, 0, 0, 0,
+ 223, 0, 0, 0, 31, 0, 159, 0,
+ 255, 0, 85, 0, 148, 0, 85, 0,
+ 127, 0, 85, 0, 106, 0, 63, 0,
+ 212, 0, 170, 0, 191, 0, 170, 0,
+ 85, 0, 42, 0, 233, 0, 21, 0,
+ };
+ const uint8_t node_3[] = {
+ 255, 0, 212, 0, 63, 0, 0, 0,
+ 106, 0, 148, 0, 85, 0, 127, 0,
+ 191, 0, 21, 0, 233, 0, 0, 0,
+ 21, 0, 170, 0, 0, 0, 42, 0,
+ 0, 0, 0, 0, 141, 0, 113, 0,
+ 255, 0, 198, 0, 0, 0, 56, 0,
+ 0, 0, 85, 0, 56, 0, 28, 0,
+ 226, 0, 28, 0, 170, 0, 56, 0,
+ 255, 0, 231, 0, 255, 0, 208, 0,
+ 139, 0, 92, 0, 115, 0, 92, 0,
+ 185, 0, 69, 0, 46, 0, 46, 0,
+ 162, 0, 23, 0, 208, 0, 46, 0,
+ };
+ const uint8_t node_4[] = {
+ 255, 0, 31, 0, 63, 0, 63, 0,
+ 127, 0, 95, 0, 191, 0, 63, 0,
+ 223, 0, 31, 0, 159, 0, 63, 0,
+ 31, 0, 63, 0, 95, 0, 31, 0,
+ 8, 0, 0, 0, 95, 0, 63, 0,
+ 255, 0, 0, 0, 127, 0, 0, 0,
+ 8, 0, 0, 0, 159, 0, 63, 0,
+ 255, 0, 223, 0, 191, 0, 31, 0,
+ 76, 0, 25, 0, 255, 0, 127, 0,
+ 153, 0, 51, 0, 204, 0, 102, 0,
+ 76, 0, 51, 0, 229, 0, 127, 0,
+ 153, 0, 51, 0, 178, 0, 102, 0,
+ };
+ const uint8_t node_5[] = {
+ 255, 0, 51, 0, 25, 0, 76, 0,
+ 0, 0, 0, 0, 102, 0, 0, 0,
+ 204, 0, 229, 0, 0, 0, 178, 0,
+ 0, 0, 153, 0, 127, 0, 8, 0,
+ 178, 0, 127, 0, 153, 0, 204, 0,
+ 255, 0, 0, 0, 25, 0, 76, 0,
+ 102, 0, 51, 0, 0, 0, 0, 0,
+ 229, 0, 25, 0, 25, 0, 204, 0,
+ 178, 0, 102, 0, 255, 0, 76, 0,
+ 127, 0, 76, 0, 229, 0, 76, 0,
+ 153, 0, 102, 0, 255, 0, 25, 0,
+ 127, 0, 51, 0, 204, 0, 51, 0,
+ };
+ const uint8_t node_6[] = {
+ 255, 0, 0, 0, 223, 0, 0, 0,
+ 31, 0, 8, 0, 127, 0, 0, 0,
+ 95, 0, 0, 0, 159, 0, 0, 0,
+ 95, 0, 63, 0, 191, 0, 0, 0,
+ 51, 0, 204, 0, 0, 0, 102, 0,
+ 255, 0, 127, 0, 8, 0, 178, 0,
+ 25, 0, 229, 0, 0, 0, 76, 0,
+ 204, 0, 153, 0, 51, 0, 25, 0,
+ 255, 0, 226, 0, 255, 0, 255, 0,
+ 198, 0, 28, 0, 141, 0, 56, 0,
+ 170, 0, 56, 0, 85, 0, 28, 0,
+ 170, 0, 28, 0, 113, 0, 56, 0,
+ };
+ const uint8_t node_7[] = {
+ 223, 0, 0, 0, 63, 0, 0, 0,
+ 95, 0, 0, 0, 223, 0, 31, 0,
+ 255, 0, 0, 0, 159, 0, 0, 0,
+ 127, 0, 31, 0, 191, 0, 31, 0,
+ 0, 0, 0, 0, 109, 0, 0, 0,
+ 218, 0, 0, 0, 182, 0, 72, 0,
+ 8, 0, 36, 0, 145, 0, 36, 0,
+ 255, 0, 8, 0, 182, 0, 72, 0,
+ 255, 0, 72, 0, 218, 0, 36, 0,
+ 218, 0, 0, 0, 145, 0, 0, 0,
+ 255, 0, 36, 0, 182, 0, 36, 0,
+ 182, 0, 0, 0, 109, 0, 0, 0,
+ };
+ const uint8_t node_8[] = {
+ 255, 0, 0, 0, 218, 0, 0, 0,
+ 36, 0, 0, 0, 218, 0, 0, 0,
+ 182, 0, 109, 0, 255, 0, 0, 0,
+ 0, 0, 0, 0, 145, 0, 72, 0,
+ 159, 0, 0, 0, 31, 0, 127, 0,
+ 255, 0, 31, 0, 0, 0, 95, 0,
+ 8, 0, 0, 0, 191, 0, 31, 0,
+ 255, 0, 31, 0, 223, 0, 63, 0,
+ 255, 0, 31, 0, 63, 0, 31, 0,
+ 95, 0, 31, 0, 63, 0, 127, 0,
+ 159, 0, 31, 0, 63, 0, 31, 0,
+ 223, 0, 223, 0, 191, 0, 191, 0,
+ };
+ const uint8_t node_9[] = {
+ 226, 0, 28, 0, 28, 0, 141, 0,
+ 8, 0, 8, 0, 255, 0, 8, 0,
+ 113, 0, 28, 0, 198, 0, 85, 0,
+ 56, 0, 198, 0, 170, 0, 28, 0,
+ 8, 0, 95, 0, 8, 0, 8, 0,
+ 255, 0, 63, 0, 31, 0, 223, 0,
+ 8, 0, 31, 0, 191, 0, 8, 0,
+ 255, 0, 127, 0, 127, 0, 159, 0,
+ 115, 0, 46, 0, 255, 0, 185, 0,
+ 139, 0, 23, 0, 208, 0, 115, 0,
+ 231, 0, 69, 0, 255, 0, 162, 0,
+ 139, 0, 115, 0, 231, 0, 92, 0,
+ };
+ const uint8_t node_10[] = {
+ 145, 0, 0, 0, 0, 0, 109, 0,
+ 0, 0, 0, 0, 255, 0, 109, 0,
+ 72, 0, 218, 0, 0, 0, 0, 0,
+ 36, 0, 0, 0, 182, 0, 0, 0,
+ 0, 0, 127, 0, 159, 0, 127, 0,
+ 159, 0, 191, 0, 223, 0, 63, 0,
+ 255, 0, 95, 0, 31, 0, 95, 0,
+ 31, 0, 8, 0, 63, 0, 8, 0,
+ 255, 0, 0, 0, 145, 0, 0, 0,
+ 182, 0, 109, 0, 109, 0, 109, 0,
+ 218, 0, 0, 0, 72, 0, 0, 0,
+ 182, 0, 72, 0, 182, 0, 36, 0,
+ };
+ const uint8_t node_11[] = {
+ 255, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 255, 0, 0, 0, 218, 0, 72, 36,
+ 0, 0, 182, 0, 0, 0, 145, 109,
+ 0, 0, 127, 0, 0, 0, 42, 0,
+ 212, 0, 0, 212, 0, 0, 212, 0,
+ 0, 0, 0, 0, 42, 0, 0, 0,
+ 255, 0, 0, 0, 170, 170, 127, 85,
+ 145, 0, 109, 109, 218, 109, 72, 0,
+ 145, 0, 72, 0, 218, 0, 109, 0,
+ 182, 0, 109, 0, 255, 0, 72, 0,
+ 182, 109, 36, 109, 255, 109, 109, 0,
+ };
+ const uint8_t node_12[] = {
+ 255, 0, 0, 0, 255, 0, 191, 0,
+ 0, 0, 0, 0, 95, 0, 63, 0,
+ 31, 0, 0, 0, 223, 0, 223, 0,
+ 0, 0, 8, 0, 159, 0, 127, 0,
+ 0, 0, 85, 0, 56, 0, 28, 0,
+ 255, 0, 28, 0, 0, 0, 226, 0,
+ 0, 0, 170, 0, 56, 0, 113, 0,
+ 198, 0, 0, 0, 113, 0, 141, 0,
+ 255, 0, 42, 0, 233, 0, 63, 0,
+ 212, 0, 85, 0, 191, 0, 106, 0,
+ 191, 0, 21, 0, 170, 0, 8, 0,
+ 170, 0, 127, 0, 148, 0, 148, 0,
+ };
+ const uint8_t node_13[] = {
+ 255, 0, 0, 0, 0, 0, 63, 0,
+ 191, 0, 95, 0, 31, 0, 223, 0,
+ 255, 0, 63, 0, 95, 0, 63, 0,
+ 159, 0, 0, 0, 0, 0, 127, 0,
+ 72, 0, 0, 0, 0, 0, 0, 0,
+ 255, 0, 0, 0, 0, 0, 0, 0,
+ 72, 0, 72, 0, 36, 0, 8, 0,
+ 218, 0, 182, 0, 145, 0, 109, 0,
+ 255, 0, 162, 0, 231, 0, 162, 0,
+ 231, 0, 115, 0, 208, 0, 139, 0,
+ 185, 0, 92, 0, 185, 0, 46, 0,
+ 162, 0, 69, 0, 162, 0, 23, 0,
+ };
+ const uint8_t node_14[] = {
+ 255, 0, 0, 0, 51, 0, 0, 0,
+ 0, 0, 0, 0, 102, 0, 0, 0,
+ 204, 0, 0, 0, 153, 0, 0, 0,
+ 0, 0, 0, 0, 51, 0, 0, 0,
+ 0, 0, 0, 0, 8, 0, 36, 0,
+ 255, 0, 0, 0, 182, 0, 8, 0,
+ 0, 0, 0, 0, 72, 0, 109, 0,
+ 145, 0, 0, 0, 255, 0, 218, 0,
+ 212, 0, 8, 0, 170, 0, 0, 0,
+ 127, 0, 0, 0, 85, 0, 8, 0,
+ 255, 0, 8, 0, 170, 0, 0, 0,
+ 127, 0, 0, 0, 42, 0, 8, 0,
+ };
+ const uint8_t node_15[] = {
+ 255, 0, 0, 0, 0, 0, 0, 0,
+ 36, 0, 0, 0, 182, 0, 0, 0,
+ 218, 0, 0, 0, 0, 0, 0, 0,
+ 72, 0, 0, 0, 145, 0, 109, 0,
+ 36, 0, 36, 0, 0, 0, 0, 0,
+ 255, 0, 0, 0, 182, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 109,
+ 218, 0, 0, 0, 145, 0, 72, 72,
+ 255, 0, 28, 0, 226, 0, 56, 0,
+ 198, 0, 0, 0, 0, 0, 28, 28,
+ 170, 0, 0, 0, 141, 0, 0, 0,
+ 113, 0, 0, 0, 85, 85, 85, 85,
+ };
+ const uint8_t node_16[] = {
+ 255, 0, 0, 0, 0, 0, 95, 0,
+ 0, 0, 127, 0, 0, 0, 0, 0,
+ 223, 0, 95, 0, 63, 0, 31, 0,
+ 191, 0, 0, 0, 159, 0, 0, 0,
+ 0, 0, 31, 0, 255, 0, 0, 0,
+ 0, 0, 95, 0, 223, 0, 0, 0,
+ 0, 0, 63, 0, 191, 0, 0, 0,
+ 0, 0, 0, 0, 159, 0, 127, 0,
+ 141, 0, 28, 0, 28, 0, 28, 0,
+ 113, 0, 8, 0, 8, 0, 8, 0,
+ 255, 0, 0, 0, 226, 0, 0, 0,
+ 198, 0, 56, 0, 170, 0, 85, 0,
+ };
+ const uint8_t node_17[] = {
+ 255, 0, 0, 0, 8, 0, 0, 0,
+ 182, 0, 0, 0, 72, 0, 0, 0,
+ 218, 0, 0, 0, 36, 0, 0, 0,
+ 145, 0, 0, 0, 109, 0, 0, 0,
+ 0, 0, 51, 25, 76, 25, 25, 0,
+ 153, 0, 0, 0, 127, 102, 178, 0,
+ 204, 0, 0, 0, 0, 0, 255, 0,
+ 0, 0, 102, 0, 229, 0, 76, 0,
+ 113, 0, 0, 0, 141, 0, 85, 0,
+ 0, 0, 0, 0, 170, 0, 0, 0,
+ 56, 28, 255, 0, 0, 0, 0, 0,
+ 198, 0, 0, 0, 226, 0, 0, 0,
+ };
+ const uint8_t node_18[] = {
+ 255, 0, 8, 0, 28, 0, 28, 0,
+ 198, 0, 56, 0, 56, 0, 85, 0,
+ 255, 0, 85, 0, 113, 0, 113, 0,
+ 226, 0, 141, 0, 170, 0, 141, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 255, 0, 0, 0, 127, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 63, 0, 0, 0, 191, 0, 0, 0,
+ 255, 0, 0, 0, 255, 0, 127, 0,
+ 0, 0, 85, 0, 0, 0, 212, 0,
+ 0, 0, 212, 0, 42, 0, 170, 0,
+ 0, 0, 127, 0, 0, 0, 0, 0,
+ };
+ const uint8_t node_19[] = {
+ 255, 0, 0, 0, 0, 0, 218, 0,
+ 182, 0, 0, 0, 0, 0, 145, 0,
+ 145, 0, 36, 0, 0, 0, 109, 0,
+ 109, 0, 0, 0, 72, 0, 36, 0,
+ 0, 0, 0, 0, 109, 0, 8, 0,
+ 72, 0, 0, 0, 255, 0, 182, 0,
+ 0, 0, 0, 0, 145, 0, 8, 0,
+ 36, 0, 8, 0, 218, 0, 182, 0,
+ 255, 0, 0, 0, 0, 0, 226, 0,
+ 85, 0, 0, 0, 141, 0, 0, 0,
+ 0, 0, 0, 0, 170, 0, 56, 0,
+ 198, 0, 0, 0, 113, 0, 28, 0,
+ };
+ const uint8_t node_20[] = {
+ 255, 0, 0, 0, 113, 0, 0, 0,
+ 198, 0, 56, 0, 85, 0, 28, 0,
+ 255, 0, 0, 0, 226, 0, 0, 0,
+ 170, 0, 0, 0, 141, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 255, 0, 145, 0, 109, 0, 218, 0,
+ 36, 0, 182, 0, 72, 0, 72, 0,
+ 255, 0, 0, 0, 0, 0, 109, 0,
+ 36, 0, 36, 0, 145, 0, 0, 0,
+ 72, 0, 72, 0, 182, 0, 0, 0,
+ 72, 0, 72, 0, 218, 0, 0, 0,
+ 109, 0, 109, 0, 255, 0, 0, 0,
+ };
+ const uint8_t node_21[] = {
+ 255, 0, 0, 0, 218, 0, 0, 0,
+ 145, 0, 0, 0, 36, 0, 0, 0,
+ 218, 0, 0, 0, 36, 0, 0, 0,
+ 182, 0, 72, 0, 0, 0, 109, 0,
+ 0, 0, 0, 0, 8, 0, 0, 0,
+ 255, 0, 85, 0, 212, 0, 42, 0,
+ 0, 0, 0, 0, 8, 0, 0, 0,
+ 85, 0, 170, 0, 127, 0, 42, 0,
+ 109, 0, 109, 0, 255, 0, 0, 0,
+ 72, 0, 72, 0, 218, 0, 0, 0,
+ 145, 0, 182, 0, 255, 0, 0, 0,
+ 36, 0, 36, 0, 218, 0, 8, 0,
+ };
+ const uint8_t node_22[] = {
+ 255, 0, 0, 0, 42, 0, 0, 0,
+ 212, 0, 0, 0, 8, 0, 212, 0,
+ 170, 0, 0, 0, 85, 0, 0, 0,
+ 212, 0, 8, 0, 127, 0, 8, 0,
+ 255, 0, 85, 0, 0, 0, 0, 0,
+ 226, 0, 85, 0, 0, 0, 198, 0,
+ 0, 0, 141, 0, 56, 0, 0, 0,
+ 170, 0, 28, 0, 0, 0, 113, 0,
+ 113, 0, 56, 0, 255, 0, 0, 0,
+ 85, 0, 56, 0, 226, 0, 0, 0,
+ 0, 0, 170, 0, 0, 0, 141, 0,
+ 28, 0, 28, 0, 198, 0, 28, 0,
+ };
+ const uint8_t node_23[] = {
+ 255, 0, 0, 0, 229, 0, 0, 0,
+ 204, 0, 204, 0, 0, 0, 76, 0,
+ 178, 0, 153, 0, 51, 0, 178, 0,
+ 178, 0, 127, 0, 102, 51, 51, 25,
+ 0, 0, 0, 0, 0, 0, 0, 31,
+ 0, 0, 0, 0, 255, 0, 0, 31,
+ 0, 0, 8, 0, 0, 0, 191, 159,
+ 127, 95, 95, 0, 223, 0, 63, 0,
+ 255, 0, 255, 0, 204, 204, 204, 204,
+ 0, 0, 51, 51, 51, 51, 0, 0,
+ 204, 0, 204, 0, 153, 153, 153, 153,
+ 153, 0, 0, 0, 102, 102, 102, 102,
+ };
+ const uint8_t node_24[] = {
+ 170, 0, 0, 0, 0, 255, 0, 0,
+ 198, 0, 0, 0, 0, 28, 0, 0,
+ 141, 0, 0, 0, 0, 226, 0, 0,
+ 56, 0, 0, 113, 0, 85, 0, 0,
+ 255, 0, 0, 0, 0, 113, 0, 0,
+ 85, 0, 0, 0, 0, 226, 0, 0,
+ 141, 0, 0, 8, 0, 170, 56, 56,
+ 198, 0, 0, 56, 0, 141, 28, 0,
+ 255, 0, 0, 0, 0, 191, 0, 0,
+ 159, 0, 0, 0, 0, 223, 0, 0,
+ 95, 0, 0, 0, 0, 63, 0, 0,
+ 127, 0, 0, 0, 0, 31, 0, 0,
+ };
+
+ static const uint8_t *drum_map[5][5] =
+ {
+ {node_10, node_8, node_0, node_9, node_11},
+ {node_15, node_7, node_13, node_12, node_6},
+ {node_18, node_14, node_4, node_5, node_3},
+ {node_23, node_16, node_21, node_1, node_2},
+ {node_24, node_19, node_17, node_20, node_22},
+ };
+
+ GridsChannel::GridsChannel()
+ {
+ }
+
+ uint8_t GridsChannel::U8Mix(uint8_t a, uint8_t b, uint8_t balance)
+ {
+ uint16_t mix = b * balance;
+ mix += (a * (255 - balance));
+ return mix / 255;
+ }
+
+ void GridsChannel::setStep(uint8_t step)
+ {
+ step_ = step;
+ }
+
+ uint8_t GridsChannel::level(int selector, uint16_t x, uint16_t y)
+ {
+ uint16_t xmap = x % 256;
+ uint16_t ymap = y % 256;
+ int part = selector % NumParts;
+ return ReadDrumMap(step_, part, xmap, ymap);
+ }
+
+ /* static */
+ uint8_t GridsChannel::ReadDrumMap(uint8_t step, uint8_t instrument, uint8_t x, uint8_t y)
+ {
+ uint8_t i = x >> 6;
+ uint8_t j = y >> 6;
+ const uint8_t *a_map = drum_map[i][j];
+ const uint8_t *b_map = drum_map[i + 1][j];
+ const uint8_t *c_map = drum_map[i][j + 1];
+ const uint8_t *d_map = drum_map[i + 1][j + 1];
+ uint8_t offset = (instrument * kStepsPerPattern) + step;
+ uint8_t a = *(a_map + offset);
+ uint8_t b = *(b_map + offset);
+ uint8_t c = *(c_map + offset);
+ uint8_t d = *(d_map + offset);
+ return U8Mix(U8Mix(a, b, x << 2), U8Mix(c, d, x << 2), y << 2);
+ }
+
+ GridsWrapper::GridsWrapper()
+ {
+ tickCount_ = 0;
+ for (auto i = 0; i < num_notes; i++)
+ {
+ midiChannels_[i] = defaultMidiChannel_;
+ noteLengths_[i] = 3;
+ channelTriggered_[i] = false;
+ density_[i] = i == 0 ? 128 : 64;
+ perturbations_[i] = 0;
+ x_[i] = 128;
+ y_[i] = 128;
+ }
+
+ accent = 128;
+ chaos = 0;
+ divider_ = 0;
+ multiplier_ = 1;
+ resMultiplier_ = 1;
+ running_ = false;
+
+ // Init default snapshot notes
+ for (int8_t s = 0; s < 8; s++)
+ {
+ for (int8_t i = 0; i < 4; i++)
+ {
+ snapshots[s].instruments[i].note = grids_notes[i];
+ }
+ }
+ }
+
+ uint32_t GridsWrapper::randomValue(uint32_t init)
+ {
+ uint32_t val = 0x12345;
+ if (init)
+ {
+ val = init;
+ return 0;
+ }
+ val = val * 214013 + 2531011;
+ return val;
+ }
+
+ void GridsWrapper::start()
+ {
+ tickCount_ = 0;
+ running_ = true;
+// MM::startClock();
+
+ nextStepTimeP_ = micros();
+ lastStepTimeP_ = micros();
+ }
+
+ void GridsWrapper::stop()
+ {
+ running_ = false;
+// MM::stopClock();
+ }
+
+ void GridsWrapper::proceed()
+ {
+ running_ = true;
+ MM::continueClock();
+ }
+
+ void GridsWrapper::setNoteOutputFunc(void (*fptr)(void *, uint8_t, MidiNoteGroup), void *context)
+ {
+ onNoteOnFuncPtr_ = fptr;
+ onNoteOnFuncPtrContext_ = context;
+ }
+
+ void GridsWrapper::onNoteOn(uint8_t gridsChannel, uint8_t channel, uint8_t noteNumber, uint8_t velocity, float stepLength, bool sendMidi, bool sendCV, uint32_t noteOnMicros)
+ {
+ if (onNoteOnFuncPtrContext_ == nullptr)
+ return;
+
+ MidiNoteGroup noteGroup;
+ noteGroup.channel = channel;
+ noteGroup.noteNumber = noteNumber;
+ noteGroup.velocity = velocity;
+ noteGroup.stepLength = stepLength;
+ noteGroup.sendMidi = sendMidi;
+ noteGroup.sendCV = sendCV;
+ noteGroup.noteonMicros = noteOnMicros;
+
+ onNoteOnFuncPtr_(onNoteOnFuncPtrContext_, gridsChannel, noteGroup);
+ }
+
+ void GridsWrapper::clockTick(uint32_t stepmicros, uint32_t microsperstep)
+ {
+ if (!running_)
+ return;
+
+ if (stepmicros >= nextStepTimeP_)
+ {
+ lastStepTimeP_ = nextStepTimeP_;
+ stepMicroDelta_ = microsperstep;
+ nextStepTimeP_ += stepMicroDelta_; // calc step based on rate
+
+ gridsTick();
+ }
+ }
+
+ // void GridsWrapper::advanceStep(uint32_t stepmicros)
+ // {
+
+ // if (steps_ == 0)
+ // {
+ // seqPos_ = 0;
+ // lastSeqPos_ = seqPos_;
+
+ // return;
+ // }
+ // lastSeqPos_ = seqPos_;
+
+ // seqPos_ = (seqPos_ + 1) % steps_;
+
+ // if (seqPos_ == 0)
+ // {
+ // startMicros = stepmicros;
+ // }
+ // }
+
+ void GridsWrapper::gridsTick()
+ {
+ if (!running_)
+ return;
+
+ uint32_t ticksPerClock = 3 << divider_;
+ bool trigger = ((tickCount_ % ticksPerClock) == 0);
+
+ uint32_t noteon_micros = micros();
+
+ if (trigger)
+ {
+ const auto step = (tickCount_ / ticksPerClock * multiplier_) % grids::kStepsPerPattern;
+ channel_.setStep(step);
+
+ if (step % 2 == 0)
+ {
+ if (swing_ < 99)
+ {
+ // clockConfig.ppqInterval = 5208 for 120 bpm
+ // 1488 for 120 bpm
+ //
+ noteon_micros = micros() + ((clockConfig.ppqInterval * resMultiplier_) / (PPQ / 24) * swing_); // full range swing
+ }
+ else if (swing_ == 99)
+ { // random drunken swing
+ uint8_t rnd_swing = rand() % 95 + 1; // rand 1 - 95 // randomly apply swing value
+ noteon_micros = micros() + ((clockConfig.ppqInterval * resMultiplier_) / (PPQ / 24) * rnd_swing);
+ }
+ }
+
+ for (auto channel = 0; channel < num_notes; channel++)
+ {
+ if (step == 0)
+ {
+ uint32_t r = randomValue();
+ perturbations_[channel] = ((r & 0xFF) * (chaos >> 2)) >> 8;
+ }
+
+ const uint8_t threshold = ~density_[channel];
+ auto level = channel_.level(channel, x_[channel], y_[channel]);
+ if (level < 255 - perturbations_[channel])
+ {
+ level += perturbations_[channel];
+ }
+
+ if (level > threshold)
+ {
+ uint8_t targetLevel = uint8_t(127.f * float(level - threshold) / float(256 - threshold));
+ uint8_t noteLevel = GridsChannel::U8Mix(127, targetLevel, accent);
+ float stepLength = kNoteLengths[noteLengths_[channel]];
+
+ onNoteOn(channel, midiChannels_[channel], grids_notes[channel], noteLevel, stepLength, true, false, noteon_micros);
+ // MM::sendNoteOn(grids_notes[channel], noteLevel, midiChannels_[channel]);
+ triggeredNotes_[channel] = grids_notes[channel];
+ channelTriggered_[channel] = true;
+ noteOffMicros_[channel] = noteon_micros + (stepLength * clockConfig.step_micros); // time at which note will be off
+ }
+ }
+ }
+ else
+ {
+ for (auto channel = 0; channel < num_notes; channel++)
+ {
+ if (channelTriggered_[channel] && noteon_micros >= noteOffMicros_[channel])
+ {
+ // MM::sendNoteOff(triggeredNotes_[channel], 0, midiChannels_[channel]);
+ // MM::sendNoteOff(grids_notes[channel], 0, midiChannels_[channel]);
+ channelTriggered_[channel] = false;
+ }
+ }
+ }
+ tickCount_++;
+ }
+
+ ChannelPatternLEDs GridsWrapper::getChannelLEDS(uint8_t channel)
+ {
+ ChannelPatternLEDs channelLeds;
+
+ // uint8_t perturbs;
+
+ for (int i = 0; i < 32; i++)
+ {
+ // const auto step = (i / ticksPerClock * multiplier_) % grids::kStepsPerPattern;
+ const auto step = i;
+ channel_.setStep(step);
+
+ if (channel < num_notes)
+ {
+
+ // if (step == 0)
+ // {
+ // uint32_t r = randomValue();
+ // perturbations_[channel] = ((r & 0xFF) * (chaos >> 2)) >> 8;
+ // }
+
+ const uint8_t threshold = ~density_[channel];
+ auto level = channel_.level(channel, x_[channel], y_[channel]);
+ if (level < 255 - perturbations_[channel])
+ {
+ level += perturbations_[channel];
+ }
+
+ if (level > threshold)
+ {
+ uint8_t targetLevel = uint8_t(127.f * float(level - threshold) / float(256 - threshold));
+ uint8_t noteLevel = GridsChannel::U8Mix(127, targetLevel, accent);
+ channelLeds.levels[i] = noteLevel;
+ // MM::sendNoteOn(grids_notes[channel], noteLevel, midiChannels_[channel]);
+ // channelTriggered_[channel] = true;
+ }
+ else
+ {
+ channelLeds.levels[i] = 0;
+ }
+ }
+ }
+
+ return channelLeds;
+ }
+
+ SnapShotSettings *GridsWrapper::getSnapShot(uint8_t snapShotIndex)
+ {
+ return &snapshots[snapShotIndex];
+ }
+
+ void GridsWrapper::setSnapShot(uint8_t snapShotIndex, SnapShotSettings snapShot)
+ {
+ snapshots[snapShotIndex] = snapShot;
+ }
+
+ void GridsWrapper::saveSnapShot(uint8_t snapShotIndex)
+ {
+ for (uint8_t i = 0; i < 4; i++)
+ {
+ snapshots[snapShotIndex].instruments[i].note = grids_notes[i];
+ snapshots[snapShotIndex].instruments[i].noteLength = noteLengths_[i];
+ snapshots[snapShotIndex].instruments[i].midiChan = midiChannels_[i];
+ snapshots[snapShotIndex].instruments[i].density = getDensity(i);
+ snapshots[snapShotIndex].instruments[i].x = getX(i);
+ snapshots[snapShotIndex].instruments[i].y = getY(i);
+ }
+
+ snapshots[snapShotIndex].accent = getAccent();
+ snapshots[snapShotIndex].resolution = resolution_;
+ snapshots[snapShotIndex].chaos = getChaos();
+ snapshots[snapShotIndex].swing = getSwing();
+
+ playingPattern = snapShotIndex;
+ }
+
+ void GridsWrapper::loadSnapShot(uint8_t snapShotIndex)
+ {
+ for (uint8_t i = 0; i < 4; i++)
+ {
+ grids_notes[i] = snapshots[snapShotIndex].instruments[i].note;
+ midiChannels_[i] = snapshots[snapShotIndex].instruments[i].midiChan;
+ noteLengths_[i] = snapshots[snapShotIndex].instruments[i].noteLength;
+ setDensity(i, snapshots[snapShotIndex].instruments[i].density);
+ setX(i, snapshots[snapShotIndex].instruments[i].x);
+ setY(i, snapshots[snapShotIndex].instruments[i].y);
+ }
+
+ setAccent(snapshots[snapShotIndex].accent);
+ setResolution(snapshots[snapShotIndex].resolution);
+ setChaos(snapshots[snapShotIndex].chaos);
+ setSwing(snapshots[snapShotIndex].swing);
+
+ playingPattern = snapShotIndex;
+ }
+
+ uint8_t GridsWrapper::getSeqPos()
+ {
+ uint32_t ticksPerClock = 3 << divider_;
+ uint8_t step = (tickCount_ / ticksPerClock * multiplier_) % grids::kStepsPerPattern;
+ return step;
+ }
+
+ bool GridsWrapper::getChannelTriggered(uint8_t chanIndex)
+ {
+ if (chanIndex < 0 || chanIndex >= num_notes)
+ return false;
+ return channelTriggered_[chanIndex];
+ }
+
+ void GridsWrapper::setMidiChan(uint8_t chanIndex, uint8_t channel)
+ {
+ if (chanIndex < 0 || chanIndex >= num_notes)
+ return;
+
+ midiChannels_[chanIndex] = channel;
+ }
+
+ uint8_t GridsWrapper::getMidiChan(uint8_t chanIndex)
+ {
+ if (chanIndex < 0 || chanIndex >= num_notes)
+ return 1;
+ return midiChannels_[chanIndex];
+ }
+
+ void GridsWrapper::setNoteLength(uint8_t channel, uint8_t newNoteLength)
+ {
+ noteLengths_[channel] = newNoteLength;
+ }
+
+ uint8_t GridsWrapper::getNoteLength(uint8_t channel)
+ {
+ return noteLengths_[channel];
+ }
+
+ void GridsWrapper::setDensity(uint8_t channel, uint8_t density)
+ {
+ density_[channel] = density;
+ }
+
+ uint8_t GridsWrapper::getDensity(uint8_t channel)
+ {
+ return density_[channel];
+ }
+
+ void GridsWrapper::setX(uint8_t channel, uint8_t x)
+ {
+ x_[channel] = x;
+ // Serial.print("setX:");
+ // Serial.print(channel);
+ // Serial.print(":");
+ // Serial.println(x);
+ }
+
+ uint8_t GridsWrapper::getX(uint8_t channel)
+ {
+ return x_[channel];
+ }
+
+ void GridsWrapper::setY(uint8_t channel, uint8_t y)
+ {
+ y_[channel] = y;
+ }
+
+ uint8_t GridsWrapper::getY(uint8_t channel)
+ {
+ return y_[channel];
+ }
+
+ void GridsWrapper::setChaos(uint8_t c)
+ {
+ chaos = c;
+ }
+
+ uint8_t GridsWrapper::getChaos()
+ {
+ return chaos;
+ }
+
+ void GridsWrapper::setResolution(uint8_t r)
+ {
+ resolution_ = r;
+ divider_ = 0;
+ if (r == 0)
+ {
+ multiplier_ = 1;
+ divider_ = 1;
+ resMultiplier_ = 0.5f;
+ }
+ else if (r == 1)
+ {
+ multiplier_ = 1;
+ resMultiplier_ = 1;
+ }
+ else if (r == 2)
+ {
+ multiplier_ = 2;
+ resMultiplier_ = 2;
+ // } else if (r == 3){
+ // multiplier_ = 4;
+ }
+ }
+
+ void GridsWrapper::setSwing(uint8_t newSwing)
+ {
+ swing_ = newSwing;
+ }
+ uint8_t GridsWrapper::getSwing()
+ {
+ return swing_;
+ }
+
+ void GridsWrapper::setAccent(uint8_t a)
+ {
+ accent = a;
+ }
+ uint8_t GridsWrapper::getAccent()
+ {
+ return accent;
+ }
+}
diff --git a/Archive/OMX-27-firmware/src/modes/retro_grids.h b/Archive/OMX-27-firmware/src/modes/retro_grids.h
new file mode 100644
index 00000000..732ee669
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/modes/retro_grids.h
@@ -0,0 +1,192 @@
+#include
+#include "../config.h"
+// #define NUM_GRIDS 8
+
+namespace grids
+{
+
+ enum Grid_Resolutions
+ {
+ HALF = 0,
+ NORMAL,
+ DOUBLE,
+ FOUR,
+ COUNT
+ };
+
+ struct InstSettings
+ {
+ uint8_t note : 7;
+ uint8_t noteLength : 4;
+ uint8_t midiChan : 5;
+ uint8_t density = 0;
+ uint8_t x = 128;
+ uint8_t y = 128;
+
+ InstSettings()
+ {
+ note = 60;
+ noteLength = 3;
+ midiChan = 1;
+ density = 0;
+ x = 128;
+ y = 128;
+ }
+ };
+
+ struct SnapShotSettings
+ {
+ InstSettings instruments[4];
+ uint8_t chaos = 0;
+ uint8_t accent = 128;
+ uint8_t resolution : 2;
+ uint8_t swing : 7;
+
+ SnapShotSettings()
+ {
+ resolution = 1;
+ swing = 0;
+ }
+ };
+
+ constexpr uint8_t kStepsPerPattern = 32;
+
+ struct ChannelPatternLEDs
+ {
+ uint8_t levels[kStepsPerPattern];
+ };
+
+ class GridsChannel
+ {
+ public:
+ GridsChannel();
+
+ void setStep(uint8_t step);
+ uint8_t level(int selector, uint16_t x, uint16_t y);
+ static uint8_t U8Mix(uint8_t a, uint8_t b, uint8_t balance);
+
+ private:
+ static uint8_t ReadDrumMap(uint8_t step, uint8_t instrument, uint8_t x, uint8_t y);
+
+ int NumParts = 4;
+ uint8_t step_;
+ };
+
+ class GridsWrapper
+ {
+ public:
+ uint8_t chaos;
+ uint8_t accent;
+
+ uint8_t grids_notes[4] = {36, 38, 42, 46};
+ static const uint8_t num_notes = sizeof(grids_notes);
+ uint8_t playingPattern = 0;
+
+ SnapShotSettings snapshots[8];
+
+ // GridPatterns gridSaves[8][4] = {
+ // {GridPatterns(), GridPatterns(), GridPatterns(), GridPatterns()},
+ // {GridPatterns(), GridPatterns(), GridPatterns(), GridPatterns()},
+ // {GridPatterns(), GridPatterns(), GridPatterns(), GridPatterns()},
+ // {GridPatterns(), GridPatterns(), GridPatterns(), GridPatterns()},
+ // {GridPatterns(), GridPatterns(), GridPatterns(), GridPatterns()},
+ // {GridPatterns(), GridPatterns(), GridPatterns(), GridPatterns()},
+ // {GridPatterns(), GridPatterns(), GridPatterns(), GridPatterns()},
+ // {GridPatterns(), GridPatterns(), GridPatterns(), GridPatterns()},
+ // };
+
+ // GridPatterns gridSaves[8][4] = {
+ // {{.density = 0, .x = 128, .y = 128}, {.density = 0, .x = 128, .y = 128}, {.density = 0, .x = 128, .y = 128}, {.density = 0, .x = 128, .y = 128}},
+ // {{.density = 0, .x = 128, .y = 128}, {.density = 0, .x = 128, .y = 128}, {.density = 0, .x = 128, .y = 128}, {.density = 0, .x = 128, .y = 128}},
+ // {{.density = 0, .x = 128, .y = 128}, {.density = 0, .x = 128, .y = 128}, {.density = 0, .x = 128, .y = 128}, {.density = 0, .x = 128, .y = 128}},
+ // {{.density = 0, .x = 128, .y = 128}, {.density = 0, .x = 128, .y = 128}, {.density = 0, .x = 128, .y = 128}, {.density = 0, .x = 128, .y = 128}},
+ // {{.density = 0, .x = 128, .y = 128}, {.density = 0, .x = 128, .y = 128}, {.density = 0, .x = 128, .y = 128}, {.density = 0, .x = 128, .y = 128}},
+ // {{.density = 0, .x = 128, .y = 128}, {.density = 0, .x = 128, .y = 128}, {.density = 0, .x = 128, .y = 128}, {.density = 0, .x = 128, .y = 128}},
+ // {{.density = 0, .x = 128, .y = 128}, {.density = 0, .x = 128, .y = 128}, {.density = 0, .x = 128, .y = 128}, {.density = 0, .x = 128, .y = 128}},
+ // {{.density = 0, .x = 128, .y = 128}, {.density = 0, .x = 128, .y = 128}, {.density = 0, .x = 128, .y = 128}, {.density = 0, .x = 128, .y = 128}}};
+
+ GridsWrapper();
+
+ void start();
+ void stop();
+ void proceed();
+ void gridsTick();
+
+ void clockTick(uint32_t stepmicros, uint32_t microsperstep);
+
+ void setNoteOutputFunc(void (*fptr)(void *, uint8_t, MidiNoteGroup), void *context);
+
+ void saveSnapShot(uint8_t snapShotIndex);
+ void loadSnapShot(uint8_t snapShotIndex);
+ SnapShotSettings *getSnapShot(uint8_t snapShotIndex);
+ void setSnapShot(uint8_t snapShotIndex, SnapShotSettings snapShot);
+
+ void setNoteLength(uint8_t channel, uint8_t newNoteLength);
+ uint8_t getNoteLength(uint8_t channel);
+
+ void setDensity(uint8_t channel, uint8_t density);
+ uint8_t getDensity(uint8_t channel);
+
+ void setX(uint8_t channel, uint8_t x);
+ uint8_t getX(uint8_t channel);
+
+ void setY(uint8_t channel, uint8_t y);
+ uint8_t getY(uint8_t channel);
+
+ void setChaos(uint8_t c);
+ uint8_t getChaos();
+
+ void setResolution(uint8_t r);
+
+ void setSwing(uint8_t newSwing);
+ uint8_t getSwing();
+
+ void setAccent(uint8_t a);
+ uint8_t getAccent();
+
+ static uint32_t randomValue(uint32_t init = 0);
+
+ ChannelPatternLEDs getChannelLEDS(uint8_t channel);
+
+ uint8_t getSeqPos();
+
+ bool getChannelTriggered(uint8_t chanIndex);
+
+ void setMidiChan(uint8_t chanIndex, uint8_t channel);
+ uint8_t getMidiChan(uint8_t chanIndex);
+
+ private:
+ GridsChannel channel_;
+ uint32_t divider_;
+ uint8_t multiplier_;
+ uint32_t tickCount_;
+ uint8_t density_[num_notes];
+ uint8_t perturbations_[num_notes];
+ uint8_t x_[num_notes];
+ uint8_t y_[num_notes];
+ uint8_t midiChannels_[num_notes];
+ uint8_t noteLengths_[num_notes];
+ uint32_t noteOffMicros_[num_notes];
+ bool channelTriggered_[num_notes];
+ uint8_t triggeredNotes_[num_notes]; // Keep track of triggered notes to avoid stuck notes
+ uint8_t resolution_;
+ uint8_t swing_ = 0;
+ bool running_;
+ float resMultiplier_ = 1;
+
+ uint8_t defaultMidiChannel_ = 1;
+
+ // Note On pointers
+ void *onNoteOnFuncPtrContext_;
+ void (*onNoteOnFuncPtr_)(void *, uint8_t, MidiNoteGroup);
+ void onNoteOn(uint8_t gridsChannel, uint8_t channel, uint8_t noteNumber, uint8_t velocity, float stepLength, bool sendMidi, bool sendCV, uint32_t noteOnMicros);
+
+ // clock values
+ Micros nextStepTimeP_ = 32;
+ Micros lastStepTimeP_ = 32;
+ uint32_t stepMicroDelta_ = 0;
+
+ // void advanceStep(uint32_t stepmicros);
+ };
+
+}
diff --git a/Archive/OMX-27-firmware/src/modes/sequencer.cpp b/Archive/OMX-27-firmware/src/modes/sequencer.cpp
new file mode 100644
index 00000000..a2442386
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/modes/sequencer.cpp
@@ -0,0 +1,897 @@
+#include
+
+#include "sequencer.h"
+#include "../config.h"
+#include "../consts/consts.h"
+#include "../consts/colors.h"
+#include "../midi/midi.h"
+#include "../midi/noteoffs.h"
+#include "../hardware/omx_disp.h"
+#include "../hardware/omx_leds.h"
+#include "../utils/omx_util.h"
+#include "../utils/cvNote_util.h"
+
+// globals in main ino
+extern SequencerState sequencer;
+// extern SysSettings sysSettings;
+
+// extern int midiChannel;
+// extern int omxSeqselectedStep;
+
+// extern Adafruit_NeoPixel strip;
+
+// extern volatile unsigned long omxseqstep_micros;
+// extern volatile unsigned long seqConfig.noteon_micros;
+// extern volatile unsigned long noteoff_micros;
+// extern volatile unsigned long ppqInterval;
+
+// extern int octave; // default C4 is 0 - range is -4 to +5
+// extern int midiKeyState[27];
+// extern bool dirtyPixels;
+// extern bool dirtyDisplay;
+// extern PendingNoteOffs pendingNoteOffs; // in noteoffs.h
+// extern int potbank;
+// extern int potValues[];
+// extern int prevPlock[];
+// extern int defaultVelocity;
+
+// funcs in main ino
+// extern void show_current_step(int patternNum);
+
+// extern StepNote* getSelectedStep();
+
+// globals from sequencer.h
+uint8_t lastNote[NUM_SEQ_PATTERNS][NUM_STEPS] = {
+ {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}};
+
+StepNote copyPatternBuffer[NUM_STEPS] = {
+ {0, 0, 0, TRIGTYPE_MUTE, {-1, -1, -1, -1, -1}, 100, 0, STEPTYPE_NONE},
+ {0, 0, 0, TRIGTYPE_MUTE, {-1, -1, -1, -1, -1}, 100, 0, STEPTYPE_NONE},
+ {0, 0, 0, TRIGTYPE_MUTE, {-1, -1, -1, -1, -1}, 100, 0, STEPTYPE_NONE},
+ {0, 0, 0, TRIGTYPE_MUTE, {-1, -1, -1, -1, -1}, 100, 0, STEPTYPE_NONE},
+ {0, 0, 0, TRIGTYPE_MUTE, {-1, -1, -1, -1, -1}, 100, 0, STEPTYPE_NONE},
+ {0, 0, 0, TRIGTYPE_MUTE, {-1, -1, -1, -1, -1}, 100, 0, STEPTYPE_NONE},
+ {0, 0, 0, TRIGTYPE_MUTE, {-1, -1, -1, -1, -1}, 100, 0, STEPTYPE_NONE},
+ {0, 0, 0, TRIGTYPE_MUTE, {-1, -1, -1, -1, -1}, 100, 0, STEPTYPE_NONE},
+ {0, 0, 0, TRIGTYPE_MUTE, {-1, -1, -1, -1, -1}, 100, 0, STEPTYPE_NONE},
+ {0, 0, 0, TRIGTYPE_MUTE, {-1, -1, -1, -1, -1}, 100, 0, STEPTYPE_NONE},
+ {0, 0, 0, TRIGTYPE_MUTE, {-1, -1, -1, -1, -1}, 100, 0, STEPTYPE_NONE},
+ {0, 0, 0, TRIGTYPE_MUTE, {-1, -1, -1, -1, -1}, 100, 0, STEPTYPE_NONE},
+ {0, 0, 0, TRIGTYPE_MUTE, {-1, -1, -1, -1, -1}, 100, 0, STEPTYPE_NONE},
+ {0, 0, 0, TRIGTYPE_MUTE, {-1, -1, -1, -1, -1}, 100, 0, STEPTYPE_NONE},
+ {0, 0, 0, TRIGTYPE_MUTE, {-1, -1, -1, -1, -1}, 100, 0, STEPTYPE_NONE},
+ {0, 0, 0, TRIGTYPE_MUTE, {-1, -1, -1, -1, -1}, 100, 0, STEPTYPE_NONE}};
+
+int loopCount[NUM_SEQ_PATTERNS][NUM_STEPS] = {
+ {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+ {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+ {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+ {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+ {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+ {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+ {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
+ {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}};
+
+const char *trigConditions[36] = {
+ "1:1",
+ "1:2", "2:2",
+ "1:3", "2:3", "3:3",
+ "1:4", "2:4", "3:4", "4:4",
+ "1:5", "2:5", "3:5", "4:5", "5:5",
+ "1:6", "2:6", "3:6", "4:6", "5:6", "6:6",
+ "1:7", "2:7", "3:7", "4:7", "5:7", "6:7", "7:7",
+ "1:8", "2:8", "3:8", "4:8", "5:8", "6:8", "7:8", "8:8"};
+
+int trigConditionsAB[36][2] = {
+ {1, 1},
+ {1, 2},
+ {2, 2},
+ {1, 3},
+ {2, 3},
+ {3, 3},
+ {1, 4},
+ {2, 4},
+ {3, 4},
+ {4, 4},
+ {1, 5},
+ {2, 5},
+ {3, 5},
+ {4, 5},
+ {5, 5},
+ {1, 6},
+ {2, 6},
+ {3, 6},
+ {4, 6},
+ {5, 6},
+ {6, 6},
+ {1, 7},
+ {2, 7},
+ {3, 7},
+ {4, 7},
+ {5, 7},
+ {6, 7},
+ {7, 7},
+ {1, 8},
+ {2, 8},
+ {3, 8},
+ {4, 8},
+ {5, 8},
+ {6, 8},
+ {7, 8},
+ {8, 8}};
+
+const char *stepTypes[STEPTYPE_COUNT] = {"--", "1", ">>", "<<", "<>", "#?", "?"};
+
+// definitions
+
+SequencerState defaultSequencer()
+{
+ auto nextStepTime = micros();
+ auto lastStepTime = micros();
+
+ auto state = SequencerState{
+ ticks : 0,
+ clockSource : 0,
+ playing : 0,
+ paused : 0,
+ stopped : 1,
+ songPosition : 0,
+ playingPattern : 0,
+ seqResetFlag : 1,
+ clockDivMult : 0,
+ stepCV : 0,
+ seq_velocity : 100,
+ seq_acc_velocity : 127,
+ lastSeqPos : {0, 0, 0, 0, 0, 0, 0, 0}, // ZERO BASED
+ seqPos : {0, 0, 0, 0, 0, 0, 0, 0}, // ZERO BASED
+ patternDefaultNoteMap : {36, 38, 37, 39, 42, 46, 49, 51}, // default to GM Drum Map for now
+ patternPage : {0, 0, 0, 0, 0, 0, 0, 0},
+ patterns : {
+ {15, 0, 0, 0, 0, 0, 1, 2, 1, 0, false, false, false, false, false},
+ {15, 1, 0, 0, 0, 0, 1, 2, 1, 0, false, false, false, false, false},
+ {15, 2, 0, 0, 0, 0, 1, 2, 1, 0, false, false, false, false, false},
+ {15, 3, 0, 0, 0, 0, 1, 2, 1, 0, false, false, false, false, false},
+ {15, 4, 0, 0, 0, 0, 1, 2, 1, 0, false, false, false, false, false},
+ {15, 5, 0, 0, 0, 0, 1, 2, 1, 0, false, false, false, false, false},
+ {15, 6, 0, 0, 0, 0, 1, 2, 1, 0, false, false, false, false, false},
+ {15, 7, 0, 0, 0, 0, 1, 2, 1, 0, false, false, false, false, false}},
+ timePerPattern : {
+ {0, nextStepTime, lastStepTime, 0},
+ {0, nextStepTime, lastStepTime, 0},
+ {0, nextStepTime, lastStepTime, 0},
+ {0, nextStepTime, lastStepTime, 0},
+ {0, nextStepTime, lastStepTime, 0},
+ {0, nextStepTime, lastStepTime, 0},
+ {0, nextStepTime, lastStepTime, 0},
+ {0, nextStepTime, lastStepTime, 0}},
+ };
+
+ return state;
+}
+
+int serializedPatternSize(bool eeprom)
+{
+ int total = sizeof(Pattern);
+ int singleStep = sizeof(StepNote);
+ int totalWithoutSteps = total - (singleStep * NUM_STEPS);
+ int numSteps = NUM_STEPS;
+
+ // 1292
+ // 1280
+ // totalWithoutSteps = 12
+ // EPROM Size = 320 + 12 = 332
+
+ // for EEPROM we only serialize 16 steps
+ if (eeprom)
+ {
+ numSteps = 16;
+ }
+ int stepSize = numSteps * singleStep;
+
+ return totalWithoutSteps + stepSize;
+}
+
+StepNote *getSelectedStep()
+{
+ return &sequencer.getCurrentPattern()->steps[seqConfig.selectedStep];
+}
+
+void step_ahead()
+{
+ // step ALL patterns ahead one place
+ for (int j = 0; j < 8; j++)
+ {
+ sequencer.lastSeqPos[j] = sequencer.seqPos[j];
+
+ sequencer.seqPos[j]++;
+ if (sequencer.seqPos[j] >= sequencer.getPatternLength(j))
+ sequencer.seqPos[j] = 0;
+
+ sequencer.patternPage[j] = getPatternPage(sequencer.seqPos[j]);
+
+ // if (sequencer.getPattern(j)->reverse) {
+ // sequencer.seqPos[j]--;
+ //// auto_reset(j); // determine whether to reset or not based on param settings
+ // if (sequencer.seqPos[j] < 0)
+ // sequencer.seqPos[j] = sequencer.getPatternLength(j)-1;
+ // } else {
+ // sequencer.seqPos[j]++;
+ //// auto_reset(j); // determine whether to reset or not based on param settings
+ // if (sequencer.seqPos[j] >= sequencer.getPatternLength(j))
+ // sequencer.seqPos[j] = 0;
+ // }
+ }
+}
+void step_back()
+{
+ // step each pattern ahead one place
+ for (int j = 0; j < 8; j++)
+ {
+ sequencer.lastSeqPos[j] = sequencer.seqPos[j];
+
+ sequencer.seqPos[j]--;
+ if (sequencer.seqPos[j] < 0)
+ sequencer.seqPos[j] = sequencer.getPatternLength(j) - 1;
+
+ sequencer.patternPage[j] = getPatternPage(sequencer.seqPos[j]);
+
+ // if (sequencer.getPattern(j)->reverse) {
+ // sequencer.seqPos[j]++;
+ // // auto_reset(j); // determine whether to reset or not based on param settings
+ // if (sequencer.seqPos[j] >= sequencer.getPatternLength(j))
+ // sequencer.seqPos[j] = 0;
+ // } else {
+ // sequencer.seqPos[j]--;
+ // // auto_reset(j);
+ // if (sequencer.seqPos[j] < 0)
+ // sequencer.seqPos[j] = sequencer.getPatternLength(j) - 1;
+ // }
+ }
+}
+
+void new_step_ahead(int patternNum)
+{
+ sequencer.lastSeqPos[patternNum] = sequencer.seqPos[patternNum];
+
+ // step ONE pattern ahead one place
+ if (sequencer.getPattern(patternNum)->reverse)
+ {
+ sequencer.seqPos[patternNum]--;
+ auto_reset(patternNum); // determine whether to reset or not based on param settings
+ }
+ else
+ {
+ sequencer.seqPos[patternNum]++;
+ auto_reset(patternNum); // determine whether to reset or not based on param settings
+ }
+}
+
+void auto_reset(int p)
+{
+ auto pattern = sequencer.getPattern(p);
+
+ // should be conditioned on whether we're in S2!!
+ if (sequencer.seqPos[p] >= sequencer.getPatternLength(p) ||
+ (pattern->autoreset && (pattern->autoresetstep > (pattern->startstep)) && (sequencer.seqPos[p] >= pattern->autoresetstep)) ||
+ (pattern->autoreset && (pattern->autoresetstep == 0) && (sequencer.seqPos[p] >= pattern->rndstep)) ||
+ (pattern->reverse && (sequencer.seqPos[p] < 0)) || // normal reverse reset
+ (pattern->reverse && pattern->autoreset && (sequencer.seqPos[p] < pattern->startstep)) // ||
+ //(settings->reverse && settings->autoreset && (settings->autoresetstep == 0 ) && (seqPos[p] < settings->rndstep))
+ )
+ {
+
+ if (pattern->reverse)
+ {
+ if (pattern->autoreset)
+ {
+ if (pattern->autoresetstep == 0)
+ {
+ sequencer.seqPos[p] = pattern->rndstep - 1;
+ }
+ else
+ {
+ sequencer.seqPos[p] = pattern->autoresetstep - 1; // resets pattern in REV
+ }
+ }
+ else
+ {
+ sequencer.seqPos[p] = (sequencer.getPatternLength(p) - pattern->startstep) - 1;
+ }
+ }
+ else
+ {
+ sequencer.seqPos[p] = (pattern->startstep); // resets pattern in FWD
+ }
+ if (pattern->autoresetfreq == pattern->current_cycle)
+ { // reset cycle logic
+ if (probResult(pattern->autoresetprob))
+ {
+ // chance of doing autoreset
+ pattern->autoreset = true;
+ }
+ else
+ {
+ pattern->autoreset = false;
+ }
+ pattern->current_cycle = 1; // reset cycle to start new iteration
+ }
+ else
+ {
+ pattern->autoreset = false;
+ pattern->current_cycle++; // advance to next cycle
+ }
+ pattern->rndstep = (rand() % sequencer.getPatternLength(p)) + 1; // randomly choose step for next cycle
+ }
+ sequencer.patternPage[p] = getPatternPage(sequencer.seqPos[p]); // FOLLOW MODE FOR SEQ PAGE
+
+ // return ()
+}
+
+bool probResult(int probSetting)
+{
+ // int tempProb = (rand() % probSetting);
+ if (probSetting == 0)
+ {
+ return false;
+ }
+ if ((rand() % 100) < probSetting)
+ { // assumes probSetting is a range 0-100
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+}
+
+bool evaluate_AB(int condition, int patternNum)
+{
+ bool shouldTrigger = false;
+ ;
+
+ loopCount[patternNum][sequencer.seqPos[patternNum]]++;
+
+ int a = trigConditionsAB[condition][0];
+ int b = trigConditionsAB[condition][1];
+
+ // Serial.print (patternNum);
+ // Serial.print ("/");
+ // Serial.print (seqPos[patternNum]);
+ // Serial.print (" ");
+ // Serial.print (loopCount[patternNum][seqPos[patternNum]]);
+ // Serial.print (" ");
+ // Serial.print (a);
+ // Serial.print (":");
+ // Serial.print (b);
+ // Serial.print (" ");
+
+ if (loopCount[patternNum][sequencer.seqPos[patternNum]] == a)
+ {
+ shouldTrigger = true;
+ }
+ else
+ {
+ shouldTrigger = false;
+ }
+ if (loopCount[patternNum][sequencer.seqPos[patternNum]] >= b)
+ {
+ loopCount[patternNum][sequencer.seqPos[patternNum]] = 0;
+ }
+ return shouldTrigger;
+}
+
+void changeStepType(int amount)
+{
+ auto tempType = getSelectedStep()->stepType + amount;
+
+ // this is fucking hacky to increment the enum for stepType
+ switch (tempType)
+ {
+ case 0:
+ getSelectedStep()->stepType = STEPTYPE_NONE;
+ break;
+ case 1:
+ getSelectedStep()->stepType = STEPTYPE_RESTART;
+ break;
+ case 2:
+ getSelectedStep()->stepType = STEPTYPE_FWD;
+ break;
+ case 3:
+ getSelectedStep()->stepType = STEPTYPE_REV;
+ break;
+ case 4:
+ getSelectedStep()->stepType = STEPTYPE_PONG;
+ break;
+ case 5:
+ getSelectedStep()->stepType = STEPTYPE_RANDSTEP;
+ break;
+ case 6:
+ getSelectedStep()->stepType = STEPTYPE_RAND;
+ break;
+ default:
+ break;
+ }
+}
+
+void step_on(int patternNum)
+{
+ // Serial.print(patternNum);
+ // Serial.println(" step on");
+ // playNote(playingPattern);
+}
+
+void step_off(int patternNum, int position)
+{
+ lastNote[patternNum][position] = 0;
+
+ // Serial.print(seqPos[patternNum]);
+ // Serial.println(" step off");
+ // analogWrite(CVPITCH_PIN, 0);
+ // digitalWrite(CVGATE_PIN, LOW);
+}
+
+void doStepS1()
+{
+ // // probability test
+ bool testProb = probResult(sequencer.getCurrentPattern()->steps[sequencer.seqPos[sequencer.playingPattern]].prob);
+
+ if (sequencer.playing)
+ {
+ unsigned long playstepmicros = micros();
+
+ for (int j = 0; j < NUM_SEQ_PATTERNS; j++)
+ { // check all patterns for notes to play in time
+ // CLOCK PER PATTERN BASED APPROACH
+ auto pattern = sequencer.getPattern(j);
+
+ // TODO: refactor timePerPattern stuff into sequencer.h
+
+ if (playstepmicros >= sequencer.timePerPattern[j].nextStepTimeP)
+ {
+
+ seqReset(); // check for seqReset
+ sequencer.timePerPattern[j].lastStepTimeP = sequencer.timePerPattern[j].nextStepTimeP;
+ sequencer.timePerPattern[j].nextStepTimeP += (clockConfig.step_micros) * (multValues[sequencer.getPattern(j)->clockDivMultP]); // calc step based on rate
+
+ sequencer.timePerPattern[j].lastPosP = (sequencer.seqPos[j] + 15) % 16;
+ if (lastNote[j][sequencer.timePerPattern[j].lastPosP] > 0)
+ {
+ step_off(j, sequencer.timePerPattern[j].lastPosP);
+ }
+ if (testProb)
+ {
+ if (evaluate_AB(pattern->steps[sequencer.seqPos[j]].condition, j))
+ {
+ if (j == sequencer.playingPattern)
+ {
+ playNote(j);
+ }
+ }
+ }
+ // No need to have this function call in here.
+ // Can put into omx_mode_sequencer and remove extern function
+ // if (j == sequencer.playingPattern)
+ // { // only show selected pattern
+ // show_current_step(sequencer.playingPattern);
+ // }
+ new_step_ahead(j);
+ }
+ }
+ }
+ // else
+ // {
+ // // show_current_step(sequencer.playingPattern);
+ // }
+}
+
+void doStepS2()
+{
+ // // probability test
+ bool testProb = probResult(sequencer.getCurrentPattern()->steps[sequencer.seqPos[sequencer.playingPattern]].prob);
+
+ if (sequencer.playing)
+ {
+ unsigned long playstepmicros = micros();
+
+ for (int j = 0; j < NUM_SEQ_PATTERNS; j++)
+ { // check all patterns for notes to play in time
+ // CLOCK PER PATTERN BASED APPROACH
+ auto pattern = sequencer.getPattern(j);
+
+ // TODO: refactor timePerPattern stuff into sequencer.h
+
+ if (playstepmicros >= sequencer.timePerPattern[j].nextStepTimeP)
+ {
+
+ seqReset(); // check for seqReset
+ sequencer.timePerPattern[j].lastStepTimeP = sequencer.timePerPattern[j].nextStepTimeP;
+ sequencer.timePerPattern[j].nextStepTimeP += (clockConfig.step_micros) * (multValues[sequencer.getPattern(j)->clockDivMultP]); // calc step based on rate
+
+ // only play if not muted
+ if (!sequencer.getPattern(j)->mute)
+ {
+ sequencer.timePerPattern[j].lastPosP = (sequencer.seqPos[j] + 15) % 16;
+ if (lastNote[j][sequencer.timePerPattern[j].lastPosP] > 0)
+ {
+ step_off(j, sequencer.timePerPattern[j].lastPosP);
+ }
+ if (testProb)
+ {
+ if (evaluate_AB(pattern->steps[sequencer.seqPos[j]].condition, j))
+ {
+ playNote(j);
+ }
+ }
+ }
+ // show_current_step(playingPattern);
+ // if (j == sequencer.playingPattern)
+ // { // only show selected pattern
+ // show_current_step(sequencer.playingPattern);
+ // }
+ new_step_ahead(j);
+ }
+ }
+ }
+ // else
+ // {
+ // show_current_step(sequencer.playingPattern);
+ // }
+}
+
+// TODO: move up to other sequencer stuff
+
+// #### SEQ Mode note on/off
+void seqNoteOn(int notenum, int velocity, int patternNum)
+{
+ int adjnote = notes[notenum] + (midiSettings.octave * 12); // adjust key for octave range
+ if (adjnote >= 0 && adjnote < 128)
+ {
+ lastNote[patternNum][sequencer.seqPos[patternNum]] = adjnote;
+ MM::sendNoteOn(adjnote, velocity, sequencer.getPatternChannel(sequencer.playingPattern));
+
+ // keep track of adjusted note when pressed so that when key is released we send
+ // the correct note off message
+ midiSettings.midiKeyState[notenum] = adjnote;
+
+ // CV
+ if (sequencer.getCurrentPattern()->sendCV)
+ {
+ cvNoteUtil.cvNoteOn(adjnote);
+ }
+ }
+
+ strip.setPixelColor(notenum, MIDINOTEON); // Set pixel's color (in RAM)
+ omxDisp.setDirty();
+ omxLeds.setDirty();
+}
+
+void seqNoteOff(int notenum, int patternNum)
+{
+ // we use the key state captured at the time we pressed the key to send the correct note off message
+ int adjnote = midiSettings.midiKeyState[notenum];
+ if (adjnote >= 0 && adjnote < 128)
+ {
+ MM::sendNoteOff(adjnote, 0, sequencer.getPatternChannel(sequencer.playingPattern));
+ // CV off
+ if (sequencer.getCurrentPattern()->sendCV)
+ {
+ cvNoteUtil.cvNoteOff(adjnote);
+ }
+ }
+
+ strip.setPixelColor(notenum, LEDOFF);
+ omxDisp.setDirty();
+ omxLeds.setDirty();
+}
+
+// Play a note / step (SEQUENCERS)
+void playNote(int patternNum)
+{
+ // Serial.println(sequencer.stepNoteP[patternNum][seqPos[patternNum]].note); // Debug
+ auto pattern = sequencer.getPattern(patternNum);
+ auto steps = pattern->steps;
+
+ bool sendnoteCV = false;
+ int rnd_swing;
+ if (sequencer.getPattern(patternNum)->sendCV)
+ {
+ sendnoteCV = true;
+ }
+ StepType playStepType = (StepType)pattern->steps[sequencer.seqPos[patternNum]].stepType;
+
+ if (steps[sequencer.seqPos[patternNum]].stepType == STEPTYPE_RAND)
+ {
+ auto tempType = random(STEPTYPE_COUNT);
+
+ // this is fucking hacky to increment the enum for stepType
+ switch (tempType)
+ {
+ case 0:
+ playStepType = STEPTYPE_NONE;
+ break;
+ case 1:
+ playStepType = STEPTYPE_RESTART;
+ break;
+ case 2:
+ playStepType = STEPTYPE_FWD;
+ break;
+ case 3:
+ playStepType = STEPTYPE_REV;
+ break;
+ case 4:
+ playStepType = STEPTYPE_PONG;
+ break;
+ case 5:
+ playStepType = STEPTYPE_RANDSTEP;
+ break;
+ }
+ // Serial.println(playStepType);
+ }
+
+ switch (playStepType)
+ {
+ case STEPTYPE_COUNT: // fall through
+ case STEPTYPE_RAND:
+ break;
+ case STEPTYPE_NONE:
+ break;
+ case STEPTYPE_FWD:
+ pattern->reverse = 0;
+ break;
+ case STEPTYPE_REV:
+ pattern->reverse = 1;
+ break;
+ case STEPTYPE_PONG:
+ pattern->reverse = !pattern->reverse;
+ break;
+ case STEPTYPE_RANDSTEP:
+ sequencer.seqPos[patternNum] = (rand() % sequencer.getPatternLength(patternNum)) + 1;
+ break;
+ case STEPTYPE_RESTART:
+ sequencer.seqPos[patternNum] = 0;
+ break;
+ break;
+ }
+
+ // regular note on trigger
+
+ if (steps[sequencer.seqPos[patternNum]].trig == TRIGTYPE_PLAY)
+ {
+ sequencer.seq_velocity = steps[sequencer.seqPos[patternNum]].vel;
+
+ uint8_t lenIndex = steps[sequencer.seqPos[patternNum]].len;
+ float noteLength = kNoteLengths[lenIndex];
+
+ // Delta = 12499.2 for 0.1 length at 120bpm
+ // Delta = 3571.2 for 0.1 length at 300bpm
+ // Delta = 8928 for 0.25 length at 300bpm
+
+ seqConfig.noteoff_micros = micros() + (uint32_t)(noteLength * clockConfig.step_micros);
+
+ if (sequencer.seqPos[patternNum] % 2 == 0)
+ {
+
+ if (pattern->swing < 99)
+ {
+ seqConfig.noteon_micros = micros() + ((clockConfig.ppqInterval * multValues[pattern->clockDivMultP]) / (PPQ / 24) * pattern->swing); // full range swing
+ // Serial.println((clockConfig.ppqInterval * multValues[settings->clockDivMultP])/(PPQ / 24) * settings->swing);
+ // } else if ((settings->swing > 50) && (settings->swing < 99)){
+ // noteon_micros = micros() + ((step_micros * multValues[settings->clockDivMultP]) * ((settings->swing - 50)* .01) ); // late swing
+ // Serial.println(((step_micros * multValues[settings->clockDivMultP]) * ((settings->swing - 50)* .01) ));
+ }
+ else if (pattern->swing == 99)
+ { // random drunken swing
+ rnd_swing = rand() % 95 + 1; // rand 1 - 95 // randomly apply swing value
+ seqConfig.noteon_micros = micros() + ((clockConfig.ppqInterval * multValues[pattern->clockDivMultP]) / (PPQ / 24) * rnd_swing);
+ }
+ }
+ else
+ {
+ seqConfig.noteon_micros = micros();
+ }
+
+ if (pendingNoteOffs.sendOffIfPresent(steps[sequencer.seqPos[patternNum]].note, sequencer.getPatternChannel(patternNum), sendnoteCV))
+ {
+ // Delay slightly so noteoff and note on are not on top of each other
+ // seqConfig.noteon_micros += 1000;
+ // seqConfig.noteoff_micros += 1000;
+ }
+
+ // Queue note-on
+ pendingNoteOns.insert(steps[sequencer.seqPos[patternNum]].note, sequencer.seq_velocity, sequencer.getPatternChannel(patternNum), seqConfig.noteon_micros, sendnoteCV);
+
+ // Pending Note Offs needs to happen after note-on
+ pendingNoteOffs.insert(steps[sequencer.seqPos[patternNum]].note, sequencer.getPatternChannel(patternNum), seqConfig.noteoff_micros, sendnoteCV);
+
+ // {notenum, vel, notelen, step_type, {p1,p2,p3,p4}, prob}
+ // send param locks
+ for (int q = 0; q < 4; q++)
+ {
+ int tempCC = steps[sequencer.seqPos[patternNum]].params[q];
+ if (tempCC > -1)
+ {
+ MM::sendControlChange(pots[potSettings.potbank][q], tempCC, sequencer.getPatternChannel(patternNum));
+ seqConfig.prevPlock[q] = tempCC;
+ }
+ else if (seqConfig.prevPlock[q] != potSettings.potValues[q])
+ {
+ // if (tempCC != seqConfig.prevPlock[q]) {
+ MM::sendControlChange(pots[potSettings.potbank][q], potSettings.potValues[q], sequencer.getPatternChannel(patternNum));
+ seqConfig.prevPlock[q] = potSettings.potValues[q];
+ }
+ }
+ lastNote[patternNum][sequencer.seqPos[patternNum]] = steps[sequencer.seqPos[patternNum]].note;
+
+ // CV is sent from pendingNoteOns/pendingNoteOffs
+ }
+}
+
+void allNotesOff()
+{
+ pendingNoteOffs.allOff();
+}
+
+void allNotesOffPanic()
+{
+#if T4
+ dac.setVoltage(0, false);
+#else
+ analogWrite(CVPITCH_PIN, 0);
+#endif
+ digitalWrite(CVGATE_PIN, LOW);
+ for (int j = 0; j < 128; j++)
+ {
+ MM::sendNoteOff(j, 0, sysSettings.midiChannel); // NEEDS FIXING
+ }
+}
+
+void transposeSeq(int patternNum, int amt)
+{
+ auto pattern = sequencer.getPattern(patternNum);
+ for (int k = 0; k < NUM_STEPS; k++)
+ {
+ pattern->steps[k].note += amt;
+ }
+}
+
+void seqReset()
+{
+ if (sequencer.seqResetFlag)
+ {
+ for (int k = 0; k < NUM_SEQ_PATTERNS; k++)
+ {
+ for (int q = 0; q < NUM_STEPS; q++)
+ {
+ loopCount[k][q] = 0;
+ }
+ if (sequencer.getPattern(k)->reverse)
+ { // REVERSE
+ sequencer.seqPos[k] = sequencer.getPatternLength(k) - 1;
+ sequencer.lastSeqPos[k] = sequencer.seqPos[k];
+ }
+ else
+ {
+ sequencer.seqPos[k] = 0;
+ sequencer.lastSeqPos[k] = sequencer.seqPos[k];
+ }
+ }
+// omxUtil.stopClocks();
+// omxUtil.startClocks();
+ // MM::stopClock();
+ // MM::startClock();
+ sequencer.seqResetFlag = false;
+ }
+}
+
+void seqStart()
+{
+ sequencer.playing = true;
+
+ for (int x = 0; x < NUM_SEQ_PATTERNS; x++)
+ {
+ sequencer.timePerPattern[x].nextStepTimeP = micros();
+ sequencer.timePerPattern[x].lastStepTimeP = micros();
+ }
+
+ if (!sequencer.seqResetFlag)
+ {
+ omxUtil.resumeClocks();
+ // MM::continueClock();
+// } else if (sequencer.seqPos[sequencer.playingPattern]==0) {
+ } else {
+ omxUtil.startClocks();
+// MM::startClock();
+ }
+}
+
+void seqStop()
+{
+ sequencer.ticks = 0;
+ sequencer.playing = false;
+ omxUtil.stopClocks();
+ // MM::stopClock();
+ allNotesOff();
+}
+
+void seqContinue()
+{
+ sequencer.playing = true;
+ omxUtil.resumeClocks();
+}
+
+int getPatternPage(int position)
+{
+ return position / NUM_STEPKEYS;
+}
+
+void rotatePattern(int patternNum, int rot)
+{
+ if (patternNum < 0 || patternNum >= NUM_SEQ_PATTERNS)
+ return;
+
+ auto pattern = sequencer.getPattern(patternNum);
+ int size = sequencer.getPatternLength(patternNum);
+ StepNote arr[size];
+ rot = (rot + size) % size;
+
+ for (int d = rot, s = 0; s < size; d = (d + 1) % size, ++s)
+ arr[d] = pattern->steps[s];
+
+ for (int i = 0; i < size; ++i)
+ pattern->steps[i] = arr[i];
+}
+
+void resetPatternDefaults(int patternNum)
+{
+ auto pattern = sequencer.getPattern(patternNum);
+
+ for (int i = 0; i < NUM_STEPS; i++)
+ {
+ // {notenum,vel,len,stepType,{p1,p2,p3,p4,p5}}
+ pattern->steps[i].note = sequencer.patternDefaultNoteMap[patternNum];
+ pattern->steps[i].len = 3;
+ }
+}
+
+void clearPattern(int patternNum)
+{
+ auto steps = sequencer.getPattern(patternNum)->steps;
+
+ for (int i = 0; i < NUM_STEPS; i++)
+ {
+ // {notenum,vel,len,stepType,{p1,p2,p3,p4,p5}}
+ steps[i].note = sequencer.patternDefaultNoteMap[patternNum];
+ steps[i].vel = midiSettings.defaultVelocity;
+ steps[i].len = 3; // Default 0.75
+ steps[i].trig = TRIGTYPE_MUTE;
+ steps[i].stepType = STEPTYPE_NONE;
+ steps[i].params[0] = -1;
+ steps[i].params[1] = -1;
+ steps[i].params[2] = -1;
+ steps[i].params[3] = -1;
+ steps[i].params[4] = -1;
+ steps[i].prob = 100;
+ steps[i].condition = 0;
+ }
+}
+
+void copyPattern(int patternNum)
+{
+ // for( int i = 0 ; i < NUM_STEPS ; ++i ){
+ // copyPatternBuffer[i] = sequencer.stepNoteP[patternNum][i];
+ // }
+ auto pattern = sequencer.getPattern(patternNum);
+ memcpy(©PatternBuffer, &pattern->steps, NUM_STEPS * sizeof(StepNote));
+}
+
+void pastePattern(int patternNum)
+{
+ // for( int i = 0 ; i < NUM_STEPS ; ++i ){
+ // sequencer.stepNoteP[patternNum][i] = copyPatternBuffer[i] ;
+ // }
+ auto pattern = sequencer.getPattern(patternNum);
+ memcpy(&pattern->steps, ©PatternBuffer, NUM_STEPS * sizeof(StepNote));
+}
+
+// global sequencer shared state
+SequencerState sequencer = defaultSequencer();
diff --git a/Archive/OMX-27-firmware/src/modes/sequencer.h b/Archive/OMX-27-firmware/src/modes/sequencer.h
new file mode 100644
index 00000000..89352019
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/modes/sequencer.h
@@ -0,0 +1,190 @@
+#pragma once
+
+#include
+#include "../config.h"
+
+#define NUM_SEQ_PATTERNS_EEPROM 6
+#define NUM_SEQ_PATTERNS 8
+#define NUM_STEPS 64
+#define NUM_STEPKEYS 16
+
+const uint8_t defaultNoteLength = 3; // index from kNoteLengths[] = {0.10, 0.25, 0.5, 0.75, 1, 1.5, 2, 4, 8, 16};
+// see config.cpp
+
+struct TimePerPattern
+{
+ Micros lastProcessTimeP : 32;
+ Micros nextStepTimeP : 32;
+ Micros lastStepTimeP : 32;
+ int lastPosP : 16;
+};
+
+enum TrigType
+{
+ TRIGTYPE_MUTE = 0,
+ TRIGTYPE_PLAY
+};
+
+enum StepType
+{
+ STEPTYPE_NONE = 0,
+ STEPTYPE_RESTART,
+ STEPTYPE_FWD,
+ STEPTYPE_REV,
+ STEPTYPE_PONG,
+ STEPTYPE_RANDSTEP,
+ STEPTYPE_RAND,
+
+ STEPTYPE_COUNT
+};
+
+extern const char *stepTypes[STEPTYPE_COUNT];
+// int stepTypeNumber[STEPTYPE_COUNT] = {STEPTYPE_NONE,STEPTYPE_RESTART,STEPTYPE_FWD,STEPTYPE_REV,STEPTYPE_RANDSTEP,STEPTYPE_RAND};
+
+struct StepNote
+{ // ?? bytes
+ uint8_t note : 7; // 0 - 127
+ // uint8_t unused : 1; // not hooked up. example of how to sneak a bool into the first byte in the structure
+ uint8_t vel : 7; // 0 - 127
+ uint8_t len : 4; // Plugs into kNoteLengths
+ TrigType trig : 1; // 0 - 1
+ int8_t params[5]; // -128 -> 127 // 40 bits
+ uint8_t prob : 7; // 0 - 100
+ uint8_t condition : 6; // 0 - 36
+ StepType stepType : 3; // can be 2 bits as long as StepType has 4 values or fewer
+
+ void CopyFrom(StepNote *other)
+ {
+ note = other->note;
+ vel = other->vel;
+ len = other->len;
+ trig = other->trig;
+
+ for (uint8_t i = 0; i < 5; i++)
+ {
+ params[i] = other->params[i];
+ }
+ prob = other->prob;
+ condition = other->condition;
+ stepType = other->stepType;
+ }
+}; // {note, vel, len, TRIG_TYPE, {params0, params1, params2, params3}, prob, cond, STEP_TYPE}
+
+struct Pattern
+{ // ?? bytes
+ uint8_t len : 6; // 0 - 63, maps to 1 - 64
+ uint8_t channel : 4; // 0 - 15 , maps to channels 1 - 16
+ uint8_t startstep : 6; // step to begin pattern. must be < patternlength-1
+ uint8_t autoresetstep : 6; // step to reset on / 0 = off
+ uint8_t autoresetfreq : 6; // tracking reset iteration if enabled / ie Freq of autoreset. should be renamed
+ uint8_t current_cycle : 6; // tracking current cycle of autoreset counter / start it at 1
+ uint8_t rndstep : 6; // for random autostep functionality
+ uint8_t clockDivMultP : 4;
+ uint8_t autoresetprob : 7; // probability of autoreset - 1 is always and totally random if autoreset is 0
+ uint8_t swing : 7;
+ bool reverse : 1;
+ bool mute : 1;
+ bool autoreset : 1; // whether autoreset is enabled
+ bool solo : 1;
+ bool sendCV : 1;
+
+ // this has to stay as the last property to ensure save/load works correctly
+ StepNote steps[NUM_STEPS]; // note data
+}; // ? bytes
+
+// holds state for sequencer
+class SequencerState
+{
+
+public:
+ int ticks; // A tick of the clock
+ bool clockSource; // Internal clock (0), external clock (1)
+ bool playing; // Are we playing?
+ bool paused; // Are we paused?
+ bool stopped; // Are we stopped? (Must init to 1)
+ byte songPosition; // A place to store the current MIDI song position
+ int playingPattern; // The currently playing pattern, 0-7
+ bool seqResetFlag; // for autoreset functionality
+ int clockDivMult; // TODO: per pattern setting
+ word stepCV;
+ int seq_velocity;
+ int seq_acc_velocity;
+
+ // TODO: move into Pattern?
+ int lastSeqPos[NUM_SEQ_PATTERNS]; // What position in the sequence are we in? ZERO BASED
+ int seqPos[NUM_SEQ_PATTERNS]; // What position in the sequence are we in? ZERO BASED
+
+ int patternDefaultNoteMap[NUM_SEQ_PATTERNS]; // default to GM Drum Map for now
+ int patternPage[NUM_SEQ_PATTERNS];
+ Pattern patterns[NUM_SEQ_PATTERNS];
+
+ // TODO: move into Pattern?
+ TimePerPattern timePerPattern[NUM_SEQ_PATTERNS];
+
+ Pattern *getPattern(int pattern)
+ {
+ return &this->patterns[pattern];
+ }
+
+ Pattern *getCurrentPattern()
+ {
+ return getPattern(this->playingPattern);
+ }
+
+ // Helpers to deal with 1-16 values for pattern length and channel when they're stored as 0-15
+ uint8_t getPatternLength(int pattern)
+ {
+ return this->patterns[pattern].len + 1;
+ }
+
+ void setPatternLength(int pattern, int len)
+ {
+ this->patterns[pattern].len = len - 1;
+ }
+
+ uint8_t getPatternChannel(int pattern)
+ {
+ return this->patterns[pattern].channel + 1;
+ }
+};
+
+extern uint8_t lastNote[NUM_SEQ_PATTERNS][NUM_STEPS];
+extern const char *trigConditions[36];
+
+// forward declarations
+SequencerState defaultSequencer();
+int serializedPatternSize(bool eeprom);
+
+StepNote *getSelectedStep();
+void doStepS1();
+void doStepS2();
+
+void transposeSeq(int patternNum, int amt);
+int getPatternPage(int position);
+void rotatePattern(int patternNum, int rot);
+
+void step_ahead();
+void step_back();
+void auto_reset(int p);
+bool probResult(int probSetting);
+
+void playNote(int patternNum);
+void seqNoteOn(int notenum, int velocity, int patternNum);
+void seqNoteOff(int notenum, int patternNum);
+void allNotesOff();
+void allNotesOffPanic(); // TODO us this used?
+
+void seqStart();
+void seqStop();
+void seqContinue();
+void seqReset();
+
+void changeStepType(int amount);
+void resetPatternDefaults(int patternNum);
+
+void copyPattern(int patternNum);
+void pastePattern(int patternNum);
+void clearPattern(int patternNum);
+
+// global sequencer shared state
+extern SequencerState sequencer;
diff --git a/Archive/OMX-27-firmware/src/modes/submodes/submode_clearstorage.cpp b/Archive/OMX-27-firmware/src/modes/submodes/submode_clearstorage.cpp
new file mode 100644
index 00000000..31ca805a
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/modes/submodes/submode_clearstorage.cpp
@@ -0,0 +1,205 @@
+#include "submode_clearstorage.h"
+#include "../../hardware/omx_disp.h"
+#include "../../hardware/omx_leds.h"
+#include "../../consts/colors.h"
+
+const char *confirmLabel = "Erase Storage?";
+const char *erasedLabel = "Erased";
+const char *restartLabel = "Restart OMX";
+const char *confirmOptions[2] = {"NO", "YES"};
+
+enum ClearStorageState
+{
+ CLRSTOR_CONFIRM,
+ CLRSTOR_RESTART
+};
+
+SubModeClearStorage::SubModeClearStorage()
+{
+ state = CLRSTOR_CONFIRM;
+ params_.addPage(2);
+}
+
+void SubModeClearStorage::onEnabled()
+{
+ state = CLRSTOR_CONFIRM;
+ params_.setSelPageAndParam(0, 0);
+ encoderSelect_ = true;
+ omxLeds.setDirty();
+ omxDisp.setDirty();
+
+ auxReleased_ = !midiSettings.keyState[0];
+}
+
+void SubModeClearStorage::onDisabled()
+{
+ strip.clear();
+ omxLeds.setDirty();
+ omxDisp.setDirty();
+}
+
+// void SubModeClearStorage::configure(SubmodePresetMode mode, uint8_t selPreset, uint8_t numPresets, bool autoSave)
+// {
+// if(selPreset >= numPresets || numPresets >= 16)
+// {
+// // Too many presets, or selPreset out of range
+// return;
+// }
+
+// this->mode = mode;
+// this->selPreset = selPreset;
+// this->numPresets = numPresets;
+// this->autoSave = autoSave;
+// }
+
+// void SubModeClearStorage::setContextPtr(void *context)
+// {
+// fptrContext_ = context;
+// }
+// void SubModeClearStorage::setDoSaveFunc(void (*fptr)(void *, uint8_t))
+// {
+// doSaveFptr_ = fptr;
+// }
+// void SubModeClearStorage::setDoLoadFunc(void (*fptr)(void *, uint8_t))
+// {
+// doLoadFptr_ = fptr;
+// }
+
+// void SubModeClearStorage::doSave(uint8_t presetIndex)
+// {
+// doSaveFptr_(fptrContext_, presetIndex);
+// }
+
+// void SubModeClearStorage::doLoad(uint8_t presetIndex)
+// {
+// doLoadFptr_(fptrContext_, presetIndex);
+// }
+
+void SubModeClearStorage::loopUpdate()
+{
+}
+
+bool SubModeClearStorage::updateLEDs()
+{
+ strip.clear();
+
+ strip.setPixelColor(0, RED);
+
+ if (state == CLRSTOR_CONFIRM)
+ {
+ bool blinkState = omxLeds.getBlinkState();
+
+ int8_t selParam = params_.getSelParam();
+
+ auto keyColor = blinkState ? (selParam == 0 ? RED : GREEN) : LEDOFF;
+
+ for (uint8_t k = 1; k < 27; k++)
+ {
+ strip.setPixelColor(k, keyColor);
+ }
+ }
+
+ return true;
+}
+
+void SubModeClearStorage::onEncoderChanged(Encoder::Update enc)
+{
+ SubmodeInterface::onEncoderChanged(enc);
+}
+
+void SubModeClearStorage::onEncoderChangedEditParam(Encoder::Update enc)
+{
+ omxDisp.setDirty();
+ omxLeds.setDirty();
+}
+
+void SubModeClearStorage::onEncoderButtonDown()
+{
+ int8_t selParam = params_.getSelParam();
+
+ if (state == CLRSTOR_CONFIRM)
+ {
+ if (selParam == 0) // No
+ {
+ omxDisp.displayMessage(exitMsg);
+ setEnabled(false);
+ return;
+ }
+ else if (selParam == 1) // Yes
+ {
+ state = CLRSTOR_RESTART;
+ eraseStorage();
+ omxDisp.displayMessage(erasedLabel);
+ return;
+ }
+ }
+
+ omxDisp.setDirty();
+ omxLeds.setDirty();
+}
+
+void SubModeClearStorage::setStoragePtr(Storage *storagePtr)
+{
+ this->storagePtr = storagePtr;
+}
+
+void SubModeClearStorage::eraseStorage()
+{
+ if (storagePtr != nullptr)
+ {
+ storagePtr->write(EEPROM_HEADER_ADDRESS + 0, 0); // Saves EEPROM_VERSION as 0 so storage is cleared on restart.
+ }
+}
+
+bool SubModeClearStorage::onKeyUpdate(OMXKeypadEvent e)
+{
+ int thisKey = e.key();
+
+ if (e.down())
+ {
+ if (thisKey == 0)
+ {
+ // Aux key to cancel and go back
+ if (auxReleased_)
+ {
+ omxDisp.displayMessage(exitMsg);
+ setEnabled(false);
+ return true;
+ }
+ return true;
+ }
+ }
+ // Key Up
+ else
+ {
+ if (thisKey == 0)
+ {
+ // Used to prevent quickly exiting if entered through aux shortcut.
+ auxReleased_ = true;
+ }
+ }
+
+ omxDisp.setDirty();
+ omxLeds.setDirty();
+
+ return true;
+}
+
+void SubModeClearStorage::onDisplayUpdate()
+{
+ if (omxDisp.isDirty())
+ {
+ if (!encoderConfig.enc_edit)
+ {
+ if (state == CLRSTOR_CONFIRM)
+ {
+ int8_t selParam = params_.getSelParam();
+ omxDisp.dispOptionCombo(confirmLabel, confirmOptions, 2, selParam, true);
+ }
+ else if (state == CLRSTOR_RESTART)
+ {
+ omxDisp.dispGenericModeLabel(restartLabel, 1, 0);
+ }
+ }
+ }
+}
diff --git a/Archive/OMX-27-firmware/src/modes/submodes/submode_clearstorage.h b/Archive/OMX-27-firmware/src/modes/submodes/submode_clearstorage.h
new file mode 100644
index 00000000..104daddc
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/modes/submodes/submode_clearstorage.h
@@ -0,0 +1,39 @@
+#pragma once
+
+#include "submode_interface.h"
+#include "../../hardware/storage.h"
+
+// Submode for saving and loading a drum kit
+class SubModeClearStorage : public SubmodeInterface
+{
+public:
+ // Constructor / deconstructor
+ SubModeClearStorage();
+ ~SubModeClearStorage() {}
+
+ void setStoragePtr(Storage *storagePtr);
+
+ // Interface methods
+ void loopUpdate() override;
+ bool updateLEDs() override;
+ void onEncoderChanged(Encoder::Update enc) override;
+ void onEncoderButtonDown() override;
+ bool onKeyUpdate(OMXKeypadEvent e) override;
+ void onDisplayUpdate() override;
+
+ bool shouldBlockEncEdit() override { return true; }
+ bool usesPots() override { return true; }
+protected:
+ // Interface methods
+ void onEnabled() override;
+ void onDisabled() override;
+ void onEncoderChangedEditParam(Encoder::Update enc) override;
+
+private:
+ uint8_t state;
+ bool auxReleased_ = false; // set to aux state onEnable, must be true to exit mode with aux.
+
+ Storage *storagePtr;
+
+ void eraseStorage();
+};
diff --git a/Archive/OMX-27-firmware/src/modes/submodes/submode_interface.cpp b/Archive/OMX-27-firmware/src/modes/submodes/submode_interface.cpp
new file mode 100644
index 00000000..fdc71a36
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/modes/submodes/submode_interface.cpp
@@ -0,0 +1,49 @@
+#include "submode_interface.h"
+#include "../../hardware/omx_disp.h"
+
+void SubmodeInterface::setEnabled(bool newEnabled)
+{
+ enabled_ = newEnabled;
+ if (enabled_)
+ {
+ onEnabled();
+ }
+ else
+ {
+ onDisabled();
+ }
+}
+bool SubmodeInterface::isEnabled()
+{
+ return enabled_;
+}
+
+bool SubmodeInterface::getEncoderSelect()
+{
+ return encoderSelect_;
+}
+
+void SubmodeInterface::onEncoderChanged(Encoder::Update enc)
+{
+ if (getEncoderSelect())
+ {
+ onEncoderChangedSelectParam(enc);
+ }
+ else
+ {
+ onEncoderChangedEditParam(enc);
+ }
+}
+
+// Handles selecting params using encoder
+void SubmodeInterface::onEncoderChangedSelectParam(Encoder::Update enc)
+{
+ params_.changeParam(enc.dir());
+ omxDisp.setDirty();
+}
+
+void SubmodeInterface::onEncoderButtonDown()
+{
+ encoderSelect_ = !encoderSelect_;
+ omxDisp.setDirty();
+}
diff --git a/Archive/OMX-27-firmware/src/modes/submodes/submode_interface.h b/Archive/OMX-27-firmware/src/modes/submodes/submode_interface.h
new file mode 100644
index 00000000..2b452d62
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/modes/submodes/submode_interface.h
@@ -0,0 +1,46 @@
+#pragma once
+#include "../../ClearUI/ClearUI_Input.h"
+#include "../../hardware/omx_keypad.h"
+#include "../../utils/param_manager.h"
+
+// defines interface for a submode, a mode within a mode
+class SubmodeInterface
+{
+public:
+ SubmodeInterface() {}
+ virtual ~SubmodeInterface() {}
+
+ virtual void onModeChanged(){};
+
+ virtual void setEnabled(bool newEnabled);
+ virtual bool isEnabled();
+
+ virtual void onPotChanged(int potIndex, int prevValue, int newValue, int analogDelta) {}
+ virtual void onClockTick() {}
+ virtual void loopUpdate() {}
+ virtual bool updateLEDs() { return true; }
+ virtual void onEncoderChanged(Encoder::Update enc);
+ virtual void onEncoderButtonDown();
+
+ virtual bool shouldBlockEncEdit() { return false; }
+
+ virtual bool onKeyUpdate(OMXKeypadEvent e) { return true; }
+ virtual bool onKeyHeldUpdate(OMXKeypadEvent e) { return true; }
+
+ virtual void onDisplayUpdate() = 0;
+
+ virtual bool usesPots() { return false; } // return true if submode uses pots
+
+ virtual bool getEncoderSelect();
+
+protected:
+ bool enabled_;
+ bool encoderSelect_;
+ ParamManager params_;
+
+ virtual void onEnabled() {} // Called whenever entering mode
+ virtual void onDisabled() {} // Called whenever exiting mode
+
+ virtual void onEncoderChangedSelectParam(Encoder::Update enc);
+ virtual void onEncoderChangedEditParam(Encoder::Update enc) = 0;
+};
diff --git a/Archive/OMX-27-firmware/src/modes/submodes/submode_midifxgroup.cpp b/Archive/OMX-27-firmware/src/modes/submodes/submode_midifxgroup.cpp
new file mode 100644
index 00000000..93e8187f
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/modes/submodes/submode_midifxgroup.cpp
@@ -0,0 +1,1629 @@
+#include "submode_midifxgroup.h"
+#include "../../hardware/omx_disp.h"
+#include "../../hardware/omx_leds.h"
+#include "../../consts/colors.h"
+#include "../../midifx/midifx_randomizer.h"
+#include "../../midifx/midifx_chance.h"
+#include "../../midifx/midifx_scaler.h"
+#include "../../midifx/midifx_monophonic.h"
+#include "../../midifx/midifx_harmonizer.h"
+#include "../../midifx/midifx_transpose.h"
+#include "../../midifx/midifx_chord.h"
+#include "../../midifx/midifx_repeat.h"
+
+// #include "midifx_arpeggiator.h"
+
+using namespace midifx;
+
+SubModeMidiFxGroup subModeMidiFx[NUM_MIDIFX_GROUPS];
+
+// const int kSelMFXColor = 0xFACAE2;
+// const int kMFXColor = ROSE;
+// const int kMFXEmptyColor = 0x600030;
+
+const int kSelMFXTypeColor = 0xE6FFCF;
+const int kMFXTypeColor = DKGREEN;
+// const int kMFXTypeEmptyColor = 0x400000;
+
+// None, Chance, Randomizer, Harmonizer = Heliotrope gray, Scaler = Spanish viridian, Monophonic = Maroon (Crayola),
+// const int kMFXTypeColors[16] = {kMFXTypeEmptyColor, CYAN, RED, 0xAA98A9, 0x007F5C, 0xC32148, kMFXTypeEmptyColor, kMFXTypeEmptyColor,
+// kMFXTypeEmptyColor, kMFXTypeEmptyColor, kMFXTypeEmptyColor, kMFXTypeEmptyColor, kMFXTypeEmptyColor, kMFXTypeEmptyColor, kMFXTypeEmptyColor, kMFXTypeEmptyColor};
+
+enum MidiFxPage
+{
+ MFXPAGE_FX,
+ MFXPAGE_FX2,
+ MFXPAGE_EXIT
+};
+
+SubModeMidiFxGroup::SubModeMidiFxGroup()
+{
+ params_.addPage(4); // 4 Midi FX slots
+ params_.addPage(4); // 4 Midi FX slots
+ params_.addPage(1); // Exit submode
+
+ for (uint8_t i = 0; i < NUM_MIDIFX_SLOTS; i++)
+ {
+ midifx_.push_back(nullptr);
+ midifxTypes_[i] = 0;
+ }
+
+ doNoteOutput_ = &SubModeMidiFxGroup::noteFuncForwarder;
+ doNoteOutputContext_ = this;
+
+ for (uint8_t i = 0; i < 32; i++)
+ {
+ onNoteGroups[i].prevNoteNumber = 255;
+ }
+}
+
+uint8_t SubModeMidiFxGroup::getArpIndex()
+{
+ if (selectedMidiFX_ < NUM_MIDIFX_SLOTS)
+ {
+ auto mfx = getMidiFX(selectedMidiFX_);
+ if(mfx != nullptr && mfx->getFXType() == MIDIFX_ARP)
+ {
+ return selectedMidiFX_;
+ }
+ }
+
+ for (uint8_t i = 0; i < NUM_MIDIFX_SLOTS; i++)
+ {
+ auto mfx = getMidiFX(i);
+ if (mfx != nullptr)
+ {
+ if (mfx->getFXType() == MIDIFX_ARP)
+ {
+ return i;
+ }
+ }
+ }
+
+ return 255;
+}
+
+midifx::MidiFXArpeggiator *SubModeMidiFxGroup::getArp(bool autoCreate)
+{
+ if (selectedMidiFX_ < NUM_MIDIFX_SLOTS)
+ {
+ auto mfx = getMidiFX(selectedMidiFX_);
+ if(mfx != nullptr && mfx->getFXType() == MIDIFX_ARP)
+ {
+ return static_cast(mfx);
+ }
+ }
+
+ bool canAddArp = false;
+ uint8_t addArpIndex = 0;
+
+ for (uint8_t i = 0; i < NUM_MIDIFX_SLOTS; i++)
+ {
+ auto mfx = getMidiFX(i);
+ if (mfx != nullptr)
+ {
+ mfx->setSelected(selected_);
+ if (mfx->getFXType() == MIDIFX_ARP)
+ {
+ return static_cast(mfx);
+ }
+ }
+ else // empty slot
+ {
+ if (!canAddArp)
+ {
+ canAddArp = true;
+ addArpIndex = i;
+ }
+ }
+ }
+
+ // If we are here, no arp was found
+
+ if (autoCreate && canAddArp)
+ {
+ changeMidiFXType(addArpIndex, MIDIFX_ARP, true);
+ return getArp(false);
+ }
+
+ return nullptr;
+}
+
+void SubModeMidiFxGroup::toggleArp()
+{
+ auto arp = getArp(true);
+ if (arp != nullptr)
+ {
+ arp->toggleArp();
+ }
+}
+
+void SubModeMidiFxGroup::toggleArpHold()
+{
+ auto arp = getArp(true);
+ if (arp != nullptr)
+ {
+ arp->toggleHold();
+ }
+}
+
+bool SubModeMidiFxGroup::isArpOn()
+{
+ auto arp = getArp(false);
+ if (arp != nullptr)
+ {
+ return arp->isOn();
+ }
+ return false;
+}
+
+bool SubModeMidiFxGroup::isArpHoldOn()
+{
+ auto arp = getArp(false);
+ if (arp != nullptr)
+ {
+ return arp->isHoldOn();
+ }
+ return false;
+}
+
+void SubModeMidiFxGroup::nextArpPattern()
+{
+ auto arp = getArp(true);
+
+ if (arp != nullptr)
+ {
+ arp->nextArpPattern();
+ }
+}
+
+void SubModeMidiFxGroup::nextArpOctRange()
+{
+ auto arp = getArp(true);
+
+ if (arp != nullptr)
+ {
+ arp->nextOctRange();
+ }
+}
+
+void SubModeMidiFxGroup::gotoArpParams()
+{
+ midiFXParamView_ = true;
+ passthroughQuickEdit = true;
+ heldMidiFX_ = -1;
+
+ getArp(true); // Create arp if empty
+
+ uint8_t arpIndex = getArpIndex();
+
+ if (arpIndex < NUM_MIDIFX_SLOTS)
+ {
+ selectedMidiFX_ = arpIndex;
+ }
+
+ omxDisp.displayMessage(mfxArpEditMsg);
+}
+
+void SubModeMidiFxGroup::enablePassthrough()
+{
+ midiFXParamView_ = true;
+ passthroughQuickEdit = true;
+ heldMidiFX_ = -1;
+
+ if (selectedMidiFX_ < NUM_MIDIFX_SLOTS)
+ {
+ auto mfx = getMidiFX(selectedMidiFX_);
+ if (mfx == nullptr)
+ {
+ selectNextMFXSlot();
+
+ if (getMidiFX(selectedMidiFX_) == nullptr)
+ {
+ omxDisp.displayMessage(mfxPassthroughEditMsg);
+ }
+ return;
+ }
+ else
+ {
+ omxDisp.displayMessage(mfx->getName());
+ return;
+ }
+ }
+
+ omxDisp.displayMessage(mfxPassthroughEditMsg);
+}
+
+void SubModeMidiFxGroup::selectPrevMFXSlot(bool silent)
+{
+ for (uint8_t i = 1; i < NUM_MIDIFX_SLOTS; i++)
+ {
+ uint8_t mfxIndex = (selectedMidiFX_ + NUM_MIDIFX_SLOTS - i) % NUM_MIDIFX_SLOTS;
+
+ if (mfxIndex != selectedMidiFX_)
+ {
+ auto mfx = getMidiFX(mfxIndex);
+ if (mfx != nullptr)
+ {
+ selectedMidiFX_ = mfxIndex;
+ if (!silent)
+ {
+ tempString = String(selectedMidiFX_ + 1) + " " + mfx->getDispName();
+ omxDisp.displayMessage(tempString);
+ }
+ return;
+ }
+ }
+ }
+}
+
+void SubModeMidiFxGroup::selectNextMFXSlot(bool silent)
+{
+ for (uint8_t i = 1; i < NUM_MIDIFX_SLOTS; i++)
+ {
+ uint8_t mfxIndex = (selectedMidiFX_ + i) % NUM_MIDIFX_SLOTS;
+
+ if (mfxIndex != selectedMidiFX_)
+ {
+ auto mfx = getMidiFX(mfxIndex);
+ if (mfx != nullptr)
+ {
+ selectedMidiFX_ = mfxIndex;
+ if (!silent)
+ {
+ tempString = String(selectedMidiFX_ + 1) + " " + mfx->getDispName();
+ omxDisp.displayMessage(tempString);
+ }
+ return;
+ }
+ }
+ }
+}
+
+uint8_t SubModeMidiFxGroup::getArpOctaveRange()
+{
+ auto arp = getArp(false);
+
+ if (arp != nullptr)
+ {
+ return arp->getOctaveRange() + 1;
+ }
+
+ return 0;
+}
+
+void SubModeMidiFxGroup::onModeChanged()
+{
+ for (uint8_t i = 0; i < NUM_MIDIFX_SLOTS; i++)
+ {
+ auto mfx = getMidiFX(i);
+ if (mfx != nullptr)
+ {
+ mfx->onModeChanged();
+ }
+ }
+}
+
+void SubModeMidiFxGroup::setSelected(bool newSelected)
+{
+ selected_ = newSelected;
+
+ for (uint8_t i = 0; i < NUM_MIDIFX_SLOTS; i++)
+ {
+ auto mfx = getMidiFX(i);
+ if (mfx != nullptr)
+ {
+ mfx->setSelected(newSelected);
+ }
+ }
+}
+
+void SubModeMidiFxGroup::setAuxDown(bool auxDown)
+{
+ auxDown_ = auxDown;
+
+ for (uint8_t i = 0; i < midifx_.size(); i++)
+ {
+ if (midifx_[i] != nullptr)
+ {
+ midifx_[i]->setAuxDown(auxDown_);
+ }
+ }
+}
+
+void SubModeMidiFxGroup::onEnabled()
+{
+ // params_.setSelPageAndParam(0, 0);
+ midiFXParamView_ = true;
+ passthroughQuickEdit = false;
+
+ // Goto first available midifx if selected one is empty.
+ auto mfx = getMidiFX(selectedMidiFX_);
+ if (mfx == nullptr)
+ {
+ for (uint8_t i = 0; i < NUM_MIDIFX_SLOTS; i++)
+ {
+ auto mfx = getMidiFX(i);
+ if (mfx != nullptr)
+ {
+ selectedMidiFX_ = i;
+ break;
+ }
+ }
+ }
+
+ heldMidiFX_ = -1;
+
+ encoderSelect_ = true;
+ omxLeds.setDirty();
+ omxDisp.setDirty();
+
+ auxReleased_ = !midiSettings.keyState[0];
+ setAuxDown(false);
+
+ // for (uint8_t i = 0; i < NUM_MIDIFX_SLOTS; i++)
+ // {
+ // auto mfx = getMidiFX(i);
+ // if (mfx != nullptr)
+ // {
+ // mfx->setEnabled(true);
+ // }
+ // }
+}
+
+void SubModeMidiFxGroup::onDisabled()
+{
+ strip.clear();
+ omxLeds.setDirty();
+ omxDisp.setDirty();
+
+ for (uint8_t i = 0; i < NUM_MIDIFX_SLOTS; i++)
+ {
+ auto mfx = getMidiFX(i);
+ if (mfx != nullptr)
+ {
+ mfx->setEnabled(false);
+ }
+ }
+}
+
+void SubModeMidiFxGroup::updateFuncKeyMode()
+{
+ auto keyState = midiSettings.keyState;
+
+ uint8_t prevMode = funcKeyMode_;
+
+ funcKeyMode_ = FUNCKEYMODE_NONE;
+
+ if(passthroughQuickEdit)
+ {
+ if (funcKeyMode_ != prevMode)
+ {
+ omxDisp.setDirty();
+ }
+ return;
+ }
+
+ if (keyState[1] && !keyState[2])
+ {
+ funcKeyMode_ = FUNCKEYMODE_F1;
+ }
+ else if (!keyState[1] && keyState[2])
+ {
+ funcKeyMode_ = FUNCKEYMODE_F2;
+ }
+ else if (keyState[1] && keyState[2])
+ {
+ funcKeyMode_ = FUNCKEYMODE_F3;
+ }
+ else
+ {
+ funcKeyMode_ = FUNCKEYMODE_NONE;
+ }
+
+ if (funcKeyMode_ != prevMode)
+ {
+ omxDisp.setDirty();
+ omxLeds.setDirty();
+ }
+}
+
+void SubModeMidiFxGroup::resync()
+{
+ for (uint8_t i = 0; i < NUM_MIDIFX_SLOTS; i++)
+ {
+ auto mfx = getMidiFX(i);
+ if (mfx != nullptr)
+ {
+ mfx->resync();
+ }
+ }
+}
+
+void SubModeMidiFxGroup::onClockTick()
+{
+ for (uint8_t i = 0; i < NUM_MIDIFX_SLOTS; i++)
+ {
+ auto mfx = getMidiFX(i);
+ if (mfx != nullptr)
+ {
+ mfx->onClockTick();
+ }
+ }
+}
+
+void SubModeMidiFxGroup::loopUpdate()
+{
+ if (enabled_)
+ {
+ updateFuncKeyMode();
+ }
+
+ for (uint8_t i = 0; i < NUM_MIDIFX_SLOTS; i++)
+ {
+ auto mfx = getMidiFX(i);
+ if (mfx != nullptr)
+ {
+ mfx->loopUpdate();
+ }
+ }
+
+ // Animation
+ if (heldMidiFX_ >= 0 && heldAnimPos_ < 100)
+ {
+ if ((micros() - prevAnimTime_) > (1000 * 10))
+ {
+ heldAnimPos_ += 1;
+ prevAnimTime_ = micros();
+ omxDisp.setDirty();
+ }
+ }
+}
+
+bool SubModeMidiFxGroup::updateLEDs()
+{
+ strip.clear();
+
+ bool blinkState = omxLeds.getBlinkState();
+ bool blinkStateSlow = omxLeds.getSlowBlinkState();
+
+ // Serial.println("MidiFX Leds");
+ auto auxColor = midiFXParamView_ ? (blinkStateSlow ? ORANGE : LEDOFF) : RED;
+ strip.setPixelColor(0, auxColor);
+
+ if (passthroughQuickEdit)
+ return false;
+
+ // for(uint8_t i = 1; i < 26; i++)
+ // {
+ // strip.setPixelColor(i, LEDOFF);
+ // }
+
+ if (midiFXParamView_)
+ {
+ auto mfx = getMidiFX(selectedMidiFX_);
+ if (mfx != nullptr && mfx->usesKeys())
+ {
+ mfx->updateLEDs(funcKeyMode_);
+ return true;
+ }
+ }
+
+ // Function Keys
+ if (funcKeyMode_ == FUNCKEYMODE_F3)
+ {
+ auto f3Color = blinkState ? LEDOFF : FUNKTHREE;
+ strip.setPixelColor(1, f3Color);
+ strip.setPixelColor(2, f3Color);
+ }
+ else
+ {
+ auto f1Color = (funcKeyMode_ == FUNCKEYMODE_F1 && blinkState) ? LEDOFF : FUNKONE;
+ strip.setPixelColor(1, f1Color);
+
+ auto f2Color = (funcKeyMode_ == FUNCKEYMODE_F2 && blinkState) ? LEDOFF : FUNKTWO;
+ strip.setPixelColor(2, f2Color);
+ }
+
+ for (uint8_t i = 0; i < NUM_MIDIFX_SLOTS; i++)
+ {
+ // auto fxColor = midiFXParamView_ ? (i == selectedMidiFX_ ? WHITE : ORANGE) : BLUE;
+
+ auto fxColor = getMidiFX(i) == nullptr ? colorConfig.midiFXEmptyColor : getMidiFX(i)->getColor();
+
+ if (i == selectedMidiFX_)
+ {
+ fxColor = blinkState ? fxColor : LEDOFF;
+ }
+
+ // auto fxColor = (i == selectedMidiFX_ ? kSelMFXColor : (getMidiFX(i) == nullptr ? kMFXEmptyColor : kMFXColor));
+
+ strip.setPixelColor(3 + i, fxColor);
+ }
+
+ // Change midifx while holding down midifx slot
+ if (heldMidiFX_ >= 0 && midiFXParamView_ && !passthroughQuickEdit)
+ {
+ uint8_t selFXType = 0;
+
+ if (getMidiFX(selectedMidiFX_) != nullptr)
+ {
+ // Serial.println("Selected MidiFX not null");
+ selFXType = getMidiFX(selectedMidiFX_)->getFXType();
+ }
+
+ for (uint8_t i = 0; i < 16; i++)
+ {
+ if (i == selFXType)
+ {
+ strip.setPixelColor(11 + i, blinkState ? colorConfig.getMidiFXColor(i) : LEDOFF);
+ }
+ else
+ {
+ strip.setPixelColor(11 + i, colorConfig.getMidiFXColor(i));
+ }
+ }
+ }
+
+ return true;
+}
+
+void SubModeMidiFxGroup::moveSelectedMidiFX(int8_t direction)
+{
+ if (direction == 0)
+ return;
+
+ uint8_t newIndex = (selectedMidiFX_ + direction + NUM_MIDIFX_SLOTS) % NUM_MIDIFX_SLOTS;
+
+ auto selMFX = getMidiFX(selectedMidiFX_);
+
+ tempMidiFX_.clear();
+
+ for (uint8_t i = 0; i < midifx_.size(); i++)
+ {
+ if (i != selectedMidiFX_)
+ {
+ tempMidiFX_.push_back(midifx_[i]);
+ }
+ }
+
+ tempMidiFX_.insert(tempMidiFX_.begin() + newIndex, selMFX);
+
+ midifx_.clear();
+
+ for (uint8_t i = 0; i < tempMidiFX_.size(); i++)
+ {
+ midifx_.push_back(tempMidiFX_[i]);
+ }
+
+ tempMidiFX_.clear();
+
+ if (midifx_.size() != NUM_MIDIFX_SLOTS)
+ {
+ Serial.println("ERROR: MidiFX size changed");
+ }
+
+ selectedMidiFX_ = newIndex;
+ reconnectInputsOutputs();
+}
+
+void SubModeMidiFxGroup::onEncoderChanged(Encoder::Update enc)
+{
+ if (midiFXParamView_)
+ {
+ if (heldMidiFX_ >= 0)
+ {
+ auto amt = constrain(enc.accel(1), -1, 1);
+
+ moveSelectedMidiFX(amt);
+ omxDisp.setDirty();
+ omxLeds.setDirty();
+ return;
+ }
+
+ if (getMidiFX(selectedMidiFX_) != nullptr)
+ {
+ getMidiFX(selectedMidiFX_)->onEncoderChanged(enc);
+ }
+ }
+ else
+ {
+ SubmodeInterface::onEncoderChanged(enc);
+ }
+}
+
+void SubModeMidiFxGroup::onEncoderChangedEditParam(Encoder::Update enc)
+{
+ omxDisp.setDirty();
+ omxLeds.setDirty();
+}
+
+void SubModeMidiFxGroup::onEncoderButtonDown()
+{
+ if (midiFXParamView_)
+ {
+ if (getMidiFX(selectedMidiFX_) != nullptr)
+ {
+ getMidiFX(selectedMidiFX_)->onEncoderButtonDown();
+ }
+ }
+ else
+ {
+ if (params_.getSelPage() == MFXPAGE_FX)
+ {
+ selectMidiFX(params_.getSelParam());
+ }
+ else if (params_.getSelPage() == MFXPAGE_FX2)
+ {
+ selectMidiFX(params_.getSelParam() + 4);
+ }
+ else if (params_.getSelPage() == MFXPAGE_EXIT && params_.getSelParam() == 0)
+ {
+ setEnabled(false);
+ }
+ else
+ {
+ SubmodeInterface::onEncoderButtonDown();
+ }
+ }
+ omxDisp.setDirty();
+ omxLeds.setDirty();
+}
+
+bool SubModeMidiFxGroup::onKeyUpdate(OMXKeypadEvent e)
+{
+ if (e.held())
+ {
+ // if(passthroughQuickEdit) return false; // Don't consume key update
+ return true;
+ }
+
+ int thisKey = e.key();
+
+ // Aux logic
+ if (thisKey == 0)
+ {
+ omxDisp.setDirty();
+ omxLeds.setDirty();
+
+ if (!auxReleased_)
+ {
+ if (!e.down())
+ {
+ // Used to prevent quickly exiting if entered through aux shortcut.
+ auxReleased_ = true;
+ }
+ }
+ else
+ {
+ if (e.down())
+ {
+ setAuxDown(true);
+ }
+ else
+ {
+ setAuxDown(false);
+ }
+
+ // exit
+ // if(!e.down() && e.clicks() == 2)
+ if (e.quickClicked())
+ {
+ passthroughQuickEdit = false;
+ midiFXParamView_ = false;
+ setEnabled(false);
+ omxDisp.displayMessage(exitMsg);
+ return true;
+ }
+ }
+
+ if (passthroughQuickEdit)
+ return false; // Don't consume key update
+ return true; // Consume key
+ }
+
+ // auto keyState = midiSettings.keyState;
+
+ bool mfxKeysActive = false;
+
+ auto mfx = getMidiFX(selectedMidiFX_);
+ if (midiFXParamView_ && mfx != nullptr && mfx->usesKeys())
+ {
+ mfxKeysActive = true;
+ }
+
+ if (e.down())
+ {
+ // if (thisKey == 0)
+ // {
+ // if(passthroughQuickEdit)
+ // {
+ // passthroughQuickEdit = false;
+ // midiFXParamView_ = false;
+ // setEnabled(false);
+ // return true;
+ // }
+ // // // Exit MidiFX view
+ // // if (midiFXParamView_)
+ // // {
+ // // midiFXParamView_ = false;
+ // // encoderSelect_ = true;
+ // // }
+ // // // Exit submode
+ // // else
+
+ // if (auxReleased_)
+ // {
+ // setEnabled(false);
+ // return true;
+ // }
+ // }
+
+ if (passthroughQuickEdit)
+ {
+ return false;
+ }
+
+ if (mfxKeysActive == false)
+ {
+ // Quick Select FX Slot
+ if (thisKey >= 3 && thisKey < 3 + NUM_MIDIFX_SLOTS)
+ {
+ if (funcKeyMode_ == FUNCKEYMODE_NONE)
+ {
+ heldMidiFX_ = thisKey - 3;
+ heldAnimPos_ = 0;
+ prevAnimTime_ = micros();
+ selectMidiFX(thisKey - 3);
+ }
+ else if (funcKeyMode_ == FUNCKEYMODE_F1)
+ {
+ // Copy
+ copyMidiFX(thisKey - 3);
+ omxDisp.displayMessage("Copy");
+ }
+ else if (funcKeyMode_ == FUNCKEYMODE_F2)
+ {
+ // Paste
+ pasteMidiFX(thisKey - 3);
+ omxDisp.displayMessage("Paste");
+ }
+ else if (funcKeyMode_ == FUNCKEYMODE_F3)
+ {
+ // Cut
+ cutMidiFX(thisKey - 3);
+ omxDisp.displayMessage("Cut");
+ }
+ }
+
+ // Change FX type
+ if (heldMidiFX_ >= 0 && midiFXParamView_ && !passthroughQuickEdit)
+ {
+ if (thisKey >= 11 && thisKey < 11 + 16)
+ {
+ changeMidiFXType(selectedMidiFX_, thisKey - 11);
+ // selectMidiFX(thisKey - 19);
+ }
+ }
+
+ if (funcKeyMode_ == FUNCKEYMODE_NONE)
+ {
+ }
+ }
+ }
+
+ // if(!e.down() && thisKey == 0)
+ // {
+ // // Used to prevent quickly exiting if entered through aux shortcut.
+ // auxReleased_ = true;
+ // }
+
+ // release held midiFX whenever a midifx key is released.
+ if (!e.down() && thisKey >= 3 && thisKey < 3 + NUM_MIDIFX_SLOTS)
+ {
+ heldMidiFX_ = -1;
+ }
+
+ if (passthroughQuickEdit)
+ {
+ return false;
+ }
+
+ if (mfxKeysActive)
+ {
+ mfx->onKeyUpdate(e, funcKeyMode_);
+ }
+
+ omxDisp.setDirty();
+ omxLeds.setDirty();
+
+ return true;
+}
+
+void SubModeMidiFxGroup::selectMidiFX(uint8_t fxIndex)
+{
+ uint8_t prevSelMFX = selectedMidiFX_;
+ midiFXParamView_ = true;
+ selectedMidiFX_ = fxIndex;
+
+ if (selectedMidiFX_ != prevSelMFX)
+ {
+ auto prevMFX = getMidiFX(prevSelMFX);
+ auto newMFX = getMidiFX(selectedMidiFX_);
+
+ if (prevMFX != nullptr)
+ {
+ prevMFX->setEnabled(false);
+ }
+
+ if (newMFX != nullptr)
+ {
+ newMFX->setEnabled(true);
+ }
+
+ passthroughQuickEdit = false;
+ }
+
+ // displayMidiFXName(fxIndex);
+}
+
+void SubModeMidiFxGroup::copyMidiFX(uint8_t fxIndex)
+{
+ if (copyBuffer != nullptr)
+ {
+ delete copyBuffer;
+ copyBuffer = nullptr;
+ }
+ auto mfx = getMidiFX(fxIndex);
+ if (mfx != nullptr)
+ {
+ copyBuffer = mfx->getClone();
+ }
+}
+void SubModeMidiFxGroup::cutMidiFX(uint8_t fxIndex)
+{
+ copyMidiFX(fxIndex);
+
+ if (getMidiFX(fxIndex) != nullptr)
+ {
+ midifx::MidiFXInterface *midifxptr = midifx_[fxIndex];
+
+ midifx_[fxIndex] = nullptr;
+
+ delete midifxptr;
+ }
+
+ midifxTypes_[fxIndex] = MIDIFX_NONE;
+ reconnectInputsOutputs();
+}
+void SubModeMidiFxGroup::pasteMidiFX(uint8_t fxIndex)
+{
+ if (getMidiFX(fxIndex) != nullptr)
+ {
+ midifx::MidiFXInterface *midifxptr = midifx_[fxIndex];
+
+ midifx_[fxIndex] = nullptr;
+
+ delete midifxptr;
+ }
+
+ if (copyBuffer != nullptr)
+ {
+ setMidiFX(fxIndex, copyBuffer->getClone());
+ }
+
+ if (getMidiFX(fxIndex) != nullptr)
+ {
+ midifxTypes_[fxIndex] = getMidiFX(fxIndex)->getFXType();
+ }
+ else
+ {
+ midifxTypes_[fxIndex] = MIDIFX_NONE;
+ }
+
+ reconnectInputsOutputs();
+}
+
+void SubModeMidiFxGroup::displayMidiFXName(uint8_t index)
+{
+ auto mfx = getMidiFX(index);
+
+ if (mfx != nullptr)
+ {
+ omxDisp.displayMessage(mfx->getName());
+ }
+ else
+ {
+ omxDisp.displayMessage("None");
+ }
+}
+
+const char *SubModeMidiFxGroup::getMFXDispName(uint8_t index)
+{
+ auto mfx = getMidiFX(index);
+
+ if (mfx != nullptr)
+ {
+ return mfx->getDispName();
+ }
+ return "-";
+}
+
+bool SubModeMidiFxGroup::getEncoderSelect()
+{
+ return encoderSelect_ && !auxDown_;
+}
+
+midifx::MidiFXInterface *SubModeMidiFxGroup::getMidiFX(uint8_t index)
+{
+ return midifx_[index];
+}
+
+void SubModeMidiFxGroup::setMidiFX(uint8_t index, midifx::MidiFXInterface *midifx)
+{
+ midifx_[index] = midifx;
+}
+
+void SubModeMidiFxGroup::changeMidiFXType(uint8_t slotIndex, uint8_t typeIndex, bool fromLoad)
+{
+ // Serial.println(String("changeMidiFXType slotIndex: ") + String(slotIndex) + " typeIndex: " + String(typeIndex));
+ if (!fromLoad)
+ {
+ if (!midiFXParamView_)
+ return;
+ }
+
+ if (typeIndex == midifxTypes_[slotIndex])
+ return;
+
+ if (getMidiFX(slotIndex) != nullptr)
+ {
+ // Serial.println("Deleting FX");
+
+ midifx::MidiFXInterface *midifxptr = midifx_[slotIndex];
+
+ midifx_[slotIndex] = nullptr;
+
+ delete midifxptr;
+ }
+
+ switch (typeIndex)
+ {
+ case MIDIFX_CHANCE:
+ {
+ setMidiFX(slotIndex, new MidiFXChance());
+ }
+ break;
+ case MIDIFX_TRANSPOSE:
+ {
+ setMidiFX(slotIndex, new MidiFXTranspose());
+ }
+ break;
+ case MIDIFX_RANDOMIZER:
+ {
+ setMidiFX(slotIndex, new MidiFXRandomizer());
+ }
+ break;
+ case MIDIFX_SELECTOR:
+ {
+ auto selector = new MidiFXSelector();
+ selector->setNoteInputFunc(slotIndex, &SubModeMidiFxGroup::midiFxSelNoteInputForwarder, this);
+ setMidiFX(slotIndex, selector);
+ }
+ break;
+ case MIDIFX_HARMONIZER:
+ {
+ setMidiFX(slotIndex, new MidiFXHarmonizer());
+ }
+ break;
+ case MIDIFX_SCALER:
+ {
+ setMidiFX(slotIndex, new MidiFXScaler());
+ }
+ break;
+ case MIDIFX_MONOPHONIC:
+ {
+ setMidiFX(slotIndex, new MidiFXMonophonic());
+ }
+ break;
+ case MIDIFX_CHORD:
+ {
+ setMidiFX(slotIndex, new MidiFXChord());
+ }
+ break;
+ case MIDIFX_REPEAT:
+ {
+ setMidiFX(slotIndex, new MidiFXRepeat());
+ }
+ break;
+ case MIDIFX_ARP:
+ {
+ setMidiFX(slotIndex, new MidiFXArpeggiator());
+ }
+ break;
+ default:
+ break;
+ }
+
+ auto mfx = getMidiFX(slotIndex);
+ if (mfx != nullptr)
+ {
+ mfx->setSelected(selected_);
+ }
+
+ if (!fromLoad)
+ {
+ displayMidiFXName(slotIndex);
+ }
+
+ midifxTypes_[slotIndex] = typeIndex;
+
+ reconnectInputsOutputs();
+}
+
+void SubModeMidiFxGroup::midiFxSelNoteInput(midifx::MidiFXSelector *mfxSelector, uint8_t midiFXIndex, MidiNoteGroup note)
+{
+ // Reconnect the outputs if the length changed.
+ // Otherwise unexpected things might happen.
+ if(mfxSelector->didLengthChange())
+ {
+ reconnectInputsOutputs();
+ }
+
+ // Serial.println("midiFxSelNoteInput");
+
+ // Note offs should go through every FX in chain
+ // if (note.noteOff)
+ // {
+ // // Serial.println("Note off, reconnecting");
+
+ // reconnectInputsOutputs();
+ // mfxSelector->handleNoteOff(note);
+ // return;
+ // }
+
+ uint8_t finalIndex = mfxSelector->getFinalMidiFXIndex(midiFXIndex);
+ // Serial.println(String("finalIndex = ") + String(finalIndex));
+
+ midifx::MidiFXInterface *finalMFX = nullptr;
+ bool finalOutputToGroup = true;
+
+ // Search for the next MFX for final mfx
+ for(uint8_t i = finalIndex; i < NUM_MIDIFX_SLOTS; i++)
+ {
+ finalMFX = getMidiFX(i);
+ if(finalMFX != nullptr)
+ {
+ // Serial.println(String("Final output: ") + String(i));
+
+ finalOutputToGroup = false;
+ break;
+ }
+ }
+
+ uint8_t length = mfxSelector->getLength();
+
+ // 0 length edge case
+ if(length == 0)
+ {
+ if(finalOutputToGroup)
+ {
+ noteOutputFunc(note);
+ }
+ else
+ {
+ finalMFX->noteInput(note);
+ }
+ return;
+ }
+
+ // For a note off, send it to all the midifx in the length
+ // however, map those outputs to the final midifx or the group output
+ if (note.noteOff)
+ {
+
+ // len = 2, mfxIndex = 1, check 2, 3, final = 4
+
+ // midifx::MidiFXInterface *firstMidiFX;
+
+ bool validMfxFound = false;
+
+ for(uint8_t i = midiFXIndex + 1; i < midiFXIndex + length + 1; i++)
+ {
+ auto mfx = getMidiFX(i);
+
+ if(mfx != nullptr)
+ {
+ validMfxFound = true;
+
+ if (finalOutputToGroup)
+ {
+ mfx->setNoteOutput(&SubModeMidiFxGroup::noteFuncForwarder, this);
+ mfx->noteInput(note);
+ }
+ else
+ {
+ mfx->setNoteOutput(&MidiFXInterface::onNoteInputForwarder, finalMFX);
+ mfx->noteInput(note);
+ }
+ }
+ }
+
+ if(!validMfxFound)
+ {
+ if (finalOutputToGroup)
+ {
+ noteOutputFunc(note);
+ return;
+ }
+ else
+ {
+ finalMFX->noteInput(note);
+ return;
+ }
+ }
+ return;
+ }
+
+ // if (finalOutputToGroup)
+ // {
+ // Serial.println("Final output is group");
+ // }
+
+ // Skip due to chance, note should go to next mfx + selector length, or master output
+ if(mfxSelector->chanceShouldSkip())
+ {
+ // Serial.println("Should Skip");
+
+ if(finalOutputToGroup)
+ {
+ noteOutputFunc(note);
+ return;
+ }
+ else
+ {
+ finalMFX->noteInput(note);
+ return;
+ }
+ }
+
+ uint8_t selIndex = mfxSelector->getSelectedMidiFXIndex(midiFXIndex);
+ // Serial.println(String("selIndex = ") + String(selIndex));
+
+ // goto final or group
+ if(selIndex == 0)
+ {
+ if(finalOutputToGroup)
+ {
+ noteOutputFunc(note);
+ return;
+ }
+ else
+ {
+ finalMFX->noteInput(note);
+ return;
+ }
+ }
+
+ // Selected index out of range
+ if(selIndex >= NUM_MIDIFX_SLOTS)
+ {
+ // Serial.println("Sel index oor");
+ noteOutputFunc(note);
+ return;
+ }
+
+ auto selMidiFX = getMidiFX(selIndex);
+
+ // Selected MFX is empty, jump to final or group output
+ if(selMidiFX == nullptr)
+ {
+ // Serial.println("Sel mfx empty");
+ if(finalOutputToGroup)
+ {
+ noteOutputFunc(note);
+ return;
+ }
+ else
+ {
+ finalMFX->noteInput(note);
+ return;
+ }
+ }
+
+ // Selected MFX is valid, remap this FX to go to final or group output
+ // Then send the note to it
+ if(selMidiFX != nullptr)
+ {
+ // Serial.println("Valid sel MFX");
+
+ if(finalOutputToGroup)
+ {
+ // Serial.println("Note to group");
+ selMidiFX->setNoteOutput(&SubModeMidiFxGroup::noteFuncForwarder, this);
+ }
+ else
+ {
+ // Serial.println("Note to final");
+ selMidiFX->setNoteOutput(&MidiFXInterface::onNoteInputForwarder, finalMFX);
+ }
+
+ selMidiFX->noteInput(note);
+ }
+}
+
+// Where the magic happens
+void SubModeMidiFxGroup::reconnectInputsOutputs()
+{
+ // Serial.println("SubModeMidiFxGroup::reconnectInputsOutputs");
+ bool validMidiFXFound = false;
+ midifx::MidiFXInterface *lastValidMidiFX = nullptr;
+
+ for (int8_t i = NUM_MIDIFX_SLOTS - 1; i >= 0; --i)
+ {
+ // Serial.println("i = " + String(i));
+
+ midifx::MidiFXInterface *fx = getMidiFX(i);
+
+ if (fx == nullptr)
+ {
+ // Serial.println("midifx is null");
+
+ continue;
+ }
+
+ fx->setSlotIndex(i);
+
+ // Last valid MidiFX, connect it's output to the main midifxgroup output
+ if (!validMidiFXFound)
+ {
+ // Serial.println("connecting midifx to main output");
+
+ fx->setNoteOutput(&SubModeMidiFxGroup::noteFuncForwarder, this);
+ lastValidMidiFX = fx;
+ validMidiFXFound = true;
+ }
+ // connect the output of this midiFX to the input of the next one
+ else
+ {
+ // if(lastValidMidiFX->getFXType() == MIDIFX_SELECTOR)
+ // {
+
+ // }
+ // else
+ // {
+ // // if(lastValidMidiFX == nullptr)
+ // // {
+ // // Serial.println("lastValidMidiFX is null");
+ // // }
+
+ // // Serial.println("connecting midifx to previous midifx");
+
+ // fx->setNoteOutput(&MidiFXInterface::onNoteInputForwarder, lastValidMidiFX);
+ // }
+
+ fx->setNoteOutput(&MidiFXInterface::onNoteInputForwarder, lastValidMidiFX);
+ lastValidMidiFX = fx;
+ }
+ }
+
+ // Connect doNoteOutput_ to the lastValidMidiFX
+ if (validMidiFXFound)
+ {
+ // Serial.println("connecting group to lastValidMidiFX");
+
+ doNoteOutput_ = &MidiFXInterface::onNoteInputForwarder;
+ doNoteOutputContext_ = lastValidMidiFX;
+ }
+ // No valid midifx, connect groups input to it's output
+ else
+ {
+ // Serial.println("connecting group to self output");
+
+ doNoteOutput_ = &SubModeMidiFxGroup::noteFuncForwarder;
+ doNoteOutputContext_ = this;
+ }
+}
+
+void SubModeMidiFxGroup::noteInput(MidiNoteGroup note)
+{
+ note.prevNoteNumber = note.noteNumber; // Cache the initial note number
+
+ if (doNoteOutputContext_ == nullptr)
+ {
+ // bypass effects, sends out
+ noteOutputFunc(note);
+ return;
+ }
+
+ // Sends to connected function ptr
+ doNoteOutput_(doNoteOutputContext_, note);
+}
+
+// Sets function pointer to send notes out of FX Group
+void SubModeMidiFxGroup::setNoteOutputFunc(void (*fptr)(void *, MidiNoteGroup), void *context)
+{
+ sendNoteOutFuncPtr_ = fptr;
+ sendNoteOutFuncPtrContext_ = context;
+}
+
+void SubModeMidiFxGroup::onPendingNoteOff(int note, int channel)
+{
+ // Serial.println("SubModeMidiFxGroup::onPendingNoteOff " + String(note) + " " + String(channel));
+
+ // find and remove notes matching description
+ for (uint8_t i = 0; i < 32; i++)
+ {
+ if (onNoteGroups[i].prevNoteNumber != 255)
+ {
+ if (onNoteGroups[i].channel == channel && onNoteGroups[i].noteNumber == note)
+ {
+ // Serial.println("found note, marking empty");
+
+ onNoteGroups[i].prevNoteNumber = 255; // mark empty
+ }
+ }
+ }
+}
+
+// Notes come here after passing through midifx
+void SubModeMidiFxGroup::noteOutputFunc(MidiNoteGroup note)
+{
+ if (note.noteOff)
+ {
+ // Serial.println("Note off");
+ // See if note was previously effected
+ // Adjust note number if it was and remove from vector
+ for (uint8_t i = 0; i < 32; i++)
+ {
+ if (onNoteGroups[i].prevNoteNumber != 255)
+ {
+ if (onNoteGroups[i].channel == note.channel && onNoteGroups[i].prevNoteNumber == note.prevNoteNumber)
+ {
+ // Serial.println("Note Found: " + String(note.prevNoteNumber));
+
+ // Send note off with adjusted note number
+
+ if (sendNoteOutFuncPtrContext_ != nullptr)
+ {
+ note.noteNumber = onNoteGroups[i].noteNumber;
+ // MidiNoteGroup noteOff = onNoteGroups[i];
+ // noteOff.noteOff = true;
+ // noteOff.velocity = 0;
+ // Serial.println("Note off sent: " + String(note.noteNumber));
+
+ sendNoteOutFuncPtr_(sendNoteOutFuncPtrContext_, note);
+ }
+ onNoteGroups[i].prevNoteNumber = 255; // mark empty
+ }
+ }
+ }
+ }
+ else if (!note.noteOff)
+ {
+ // Serial.println("Note on");
+
+ // Keep track of note, up to 32 notes tracked at once
+ for (uint8_t i = 0; i < 32; i++)
+ {
+ if (onNoteGroups[i].prevNoteNumber == 255)
+ {
+ // Serial.println("Found empty slot: " + String(note.prevNoteNumber));
+
+ // onNoteGroups[i] = note;
+ onNoteGroups[i].channel = note.channel;
+ onNoteGroups[i].prevNoteNumber = note.prevNoteNumber;
+ onNoteGroups[i].noteNumber = note.noteNumber;
+
+ // Send the note out of FX group
+ if (sendNoteOutFuncPtrContext_ != nullptr)
+ {
+ // Serial.println("Note on sent: " + String(note.noteNumber));
+ sendNoteOutFuncPtr_(sendNoteOutFuncPtrContext_, note);
+ }
+
+ return;
+ }
+ }
+ }
+}
+
+void SubModeMidiFxGroup::setupPageLegends()
+{
+ omxDisp.clearLegends();
+
+ // omxDisp.dispPage = page + 1;
+
+ int8_t page = params_.getSelPage();
+
+ switch (page)
+ {
+ case MFXPAGE_FX:
+ {
+ omxDisp.legends[0] = "FX 1";
+ omxDisp.legends[1] = "FX 2";
+ omxDisp.legends[2] = "FX 3";
+ omxDisp.legends[3] = "FX 4";
+ omxDisp.legendVals[0] = -127;
+ omxDisp.legendVals[1] = -127;
+ omxDisp.legendVals[2] = -127;
+ omxDisp.legendVals[3] = -127;
+ omxDisp.legendText[0] = getMFXDispName(0);
+ omxDisp.legendText[1] = getMFXDispName(1);
+ omxDisp.legendText[2] = getMFXDispName(2);
+ omxDisp.legendText[3] = getMFXDispName(3);
+ }
+ break;
+ case MFXPAGE_FX2:
+ {
+ omxDisp.legends[0] = "FX 5";
+ omxDisp.legends[1] = "FX 6";
+ omxDisp.legends[2] = "FX 7";
+ omxDisp.legends[3] = "FX 8";
+ omxDisp.legendVals[0] = -127;
+ omxDisp.legendVals[1] = -127;
+ omxDisp.legendVals[2] = -127;
+ omxDisp.legendVals[3] = -127;
+ omxDisp.legendText[0] = getMFXDispName(4);
+ omxDisp.legendText[1] = getMFXDispName(5);
+ omxDisp.legendText[2] = getMFXDispName(6);
+ omxDisp.legendText[3] = getMFXDispName(7);
+ }
+ break;
+ case MFXPAGE_EXIT:
+ {
+ omxDisp.legends[0] = "Exit";
+ omxDisp.legends[1] = "";
+ omxDisp.legends[2] = "";
+ omxDisp.legends[3] = "";
+ omxDisp.legendVals[0] = -127;
+ omxDisp.legendVals[1] = -127;
+ omxDisp.legendVals[2] = -127;
+ omxDisp.legendVals[3] = -127;
+ omxDisp.legendText[0] = "Exit";
+ omxDisp.legendText[1] = "";
+ omxDisp.legendText[2] = "";
+ omxDisp.legendText[3] = "";
+ }
+ break;
+ default:
+ break;
+ }
+}
+
+void SubModeMidiFxGroup::onDisplayUpdateMidiFX()
+{
+ if (heldMidiFX_ >= 0)
+ {
+ const char *slotNames[NUM_MIDIFX_SLOTS];
+
+ for (uint8_t i = 0; i < NUM_MIDIFX_SLOTS; i++)
+ {
+ auto mfx = getMidiFX(i);
+ if (mfx == nullptr)
+ {
+ slotNames[i] = "-";
+ }
+ else
+ {
+ slotNames[i] = mfx->getDispName();
+ }
+ }
+
+ omxDisp.dispSlots(slotNames, NUM_MIDIFX_SLOTS, selectedMidiFX_, heldAnimPos_, getEncoderSelect(), false, nullptr, 0);
+ return;
+ }
+
+ MidiFXInterface *selFX = getMidiFX(selectedMidiFX_);
+
+ bool mfxKeysActive = midiFXParamView_ && selFX != nullptr && selFX->usesKeys();
+
+ if (!mfxKeysActive && funcKeyMode_ == FUNCKEYMODE_F1)
+ {
+ omxDisp.dispGenericModeLabel("Copy", params_.getNumPages(), params_.getSelPage());
+ }
+ else if (!mfxKeysActive && funcKeyMode_ == FUNCKEYMODE_F2)
+ {
+ omxDisp.dispGenericModeLabel("Paste", params_.getNumPages(), params_.getSelPage());
+ }
+ else if (!mfxKeysActive && funcKeyMode_ == FUNCKEYMODE_F3)
+ {
+ omxDisp.dispGenericModeLabel("Cut", params_.getNumPages(), params_.getSelPage());
+ }
+ else
+ {
+ if (selFX == nullptr)
+ {
+ omxDisp.displayMessage("No FX");
+ }
+ else
+ {
+ selFX->onDisplayUpdate(funcKeyMode_);
+ }
+ }
+}
+
+void SubModeMidiFxGroup::onDisplayUpdate()
+{
+ // omxLeds.updateBlinkStates();
+
+ // if (omxLeds.isDirty())
+ // {
+ // updateLEDs();
+ // }
+
+ if (omxDisp.isDirty())
+ {
+ if (!encoderConfig.enc_edit)
+ {
+
+ if (midiFXParamView_)
+ {
+ onDisplayUpdateMidiFX();
+ }
+ else
+ {
+ setupPageLegends();
+ omxDisp.dispGenericMode2(params_.getNumPages(), params_.getSelPage(), params_.getSelParam(), getEncoderSelect());
+ }
+ }
+ }
+}
+
+int SubModeMidiFxGroup::saveToDisk(int startingAddress, Storage *storage)
+{
+ for (uint8_t i = 0; i < NUM_MIDIFX_SLOTS; i++)
+ {
+ MidiFXInterface *mFX = getMidiFX(i);
+
+ if (mFX == nullptr)
+ {
+ // Serial.println("NoMFX");
+ storage->write(startingAddress, MIDIFX_NONE);
+ startingAddress++;
+ }
+ else
+ {
+ int mfxType = mFX->getFXType();
+ // Serial.println((String)"MFX: " + mfxType);
+ storage->write(startingAddress, mfxType);
+ startingAddress++;
+
+ startingAddress = mFX->saveToDisk(startingAddress, storage);
+ }
+
+ // Serial.println((String)"startingAddress: " + startingAddress);
+ }
+
+ return startingAddress;
+}
+
+int SubModeMidiFxGroup::loadFromDisk(int startingAddress, Storage *storage)
+{
+ for (uint8_t i = 0; i < NUM_MIDIFX_SLOTS; i++)
+ {
+ int mfxType = storage->read(startingAddress);
+ startingAddress++;
+
+ if(mfxType >= 0 && mfxType)
+
+ // Serial.println((String)"MFX: " + mfxType);
+
+ changeMidiFXType(i, mfxType, true);
+
+ MidiFXInterface *mFX = getMidiFX(i);
+
+ if (mFX != nullptr)
+ {
+ startingAddress = mFX->loadFromDisk(startingAddress, storage);
+ }
+ else
+ {
+ // Serial.println("mfx is null");
+ }
+
+ // Serial.println((String)"startingAddress: " + startingAddress);
+ }
+
+ return startingAddress;
+}
diff --git a/Archive/OMX-27-firmware/src/modes/submodes/submode_midifxgroup.h b/Archive/OMX-27-firmware/src/modes/submodes/submode_midifxgroup.h
new file mode 100644
index 00000000..43be816a
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/modes/submodes/submode_midifxgroup.h
@@ -0,0 +1,156 @@
+#pragma once
+
+#include "submode_interface.h"
+#include "../../midifx/midifx_interface.h"
+#include "../../hardware/storage.h"
+#include "../../midifx/midifx_arpeggiator.h"
+#include "../../midifx/midifx_selector.h"
+
+#define NUM_MIDIFX_GROUPS 5
+#define NUM_MIDIFX_SLOTS 8
+
+// Holds a group of 4 midi fx slots.
+class SubModeMidiFxGroup : public SubmodeInterface
+{
+public:
+ // Constructor / deconstructor
+ SubModeMidiFxGroup();
+ ~SubModeMidiFxGroup() {}
+
+ // Interface methods
+ void onModeChanged() override;
+ void onClockTick() override;
+ void loopUpdate() override;
+ void resync();
+ bool updateLEDs() override;
+ void onEncoderChanged(Encoder::Update enc);
+ void onEncoderButtonDown() override;
+ bool onKeyUpdate(OMXKeypadEvent e) override;
+ void onDisplayUpdate() override;
+ bool getEncoderSelect() override;
+
+ void setSelected(bool newSelected);
+
+ void noteInput(MidiNoteGroup note);
+ void setNoteOutputFunc(void (*fptr)(void *, MidiNoteGroup), void *context);
+
+ void onPendingNoteOff(int note, int channel);
+
+ int saveToDisk(int startingAddress, Storage *storage);
+ int loadFromDisk(int startingAddress, Storage *storage);
+
+ void toggleArp();
+ void toggleArpHold();
+ bool isArpOn();
+ bool isArpHoldOn();
+ void nextArpPattern();
+ void nextArpOctRange();
+ void gotoArpParams();
+ void enablePassthrough();
+ void selectPrevMFXSlot(bool silent = false);
+ void selectNextMFXSlot(bool silent = false);
+
+
+ uint8_t getArpOctaveRange();
+
+ midifx::MidiFXArpeggiator *getArp(bool autoCreate);
+
+protected:
+ // Interface methods
+ void onEnabled() override;
+ void onDisabled() override;
+ void onEncoderChangedEditParam(Encoder::Update enc) override;
+
+private:
+ bool selected_ = false;
+ bool midiFXParamView_ = false; // If true, parameters adjust the selected midiFX slot.
+ bool passthroughQuickEdit = false; // If true, parameters adjust the selected midiFX slot.
+
+ uint8_t selectedMidiFX_ = 0; // Index of selected midiFX slot
+
+ int8_t heldMidiFX_ = -1;
+ uint8_t heldAnimPos_ = 0;
+ Micros prevAnimTime_;
+
+ uint8_t funcKeyMode_ = 0;
+
+ bool auxDown_ = false; // set to aux state onEnable, must be true to exit mode with aux.
+
+ bool auxReleased_ = false; // set to aux state onEnable, must be true to exit mode with aux.
+
+ // typedef midifx::MidiFXInterface* MidiFXptr;
+
+ std::vector midifx_;
+
+ std::vector tempMidiFX_;
+
+ // midifx::MidiFXInterface* midiFX1_ = nullptr;
+ // midifx::MidiFXInterface* midiFX2_ = nullptr;
+ // midifx::MidiFXInterface* midiFX3_ = nullptr;
+ // midifx::MidiFXInterface* midiFX4_ = nullptr;
+
+ // MidiFXptr* midifx_[4] = {nullptr, nullptr, nullptr, nullptr};
+
+ uint8_t midifxTypes_[NUM_MIDIFX_SLOTS];
+
+ MidiNoteGroup onNoteGroups[32];
+
+ midifx::MidiFXInterface *getMidiFX(uint8_t index);
+
+ void setMidiFX(uint8_t index, midifx::MidiFXInterface *midifx);
+ uint8_t getArpIndex();
+ void setupPageLegends();
+ void setAuxDown(bool auxDown);
+
+ void updateFuncKeyMode();
+
+ void onDisplayUpdateMidiFX();
+
+ void displayMidiFXName(uint8_t index);
+
+ const char *getMFXDispName(uint8_t index);
+
+ void selectMidiFX(uint8_t fxIndex);
+ void changeMidiFXType(uint8_t slotIndex, uint8_t typeIndex, bool fromLoad = false);
+
+ void copyMidiFX(uint8_t fxIndex);
+ void cutMidiFX(uint8_t fxIndex);
+ void pasteMidiFX(uint8_t fxIndex);
+
+ void moveSelectedMidiFX(int8_t direction);
+
+ midifx::MidiFXInterface *copyBuffer;
+
+ // Static glue to link a pointer to a member function
+ static void noteFuncForwarder(void *context, MidiNoteGroup note)
+ {
+ static_cast(context)->noteOutputFunc(note);
+ }
+
+ // sends the final notes out of midifx
+ void noteOutputFunc(MidiNoteGroup note);
+
+ // Pointer to external function that notes are sent out of fxgroup to
+ void *sendNoteOutFuncPtrContext_;
+ void (*sendNoteOutFuncPtr_)(void *, MidiNoteGroup);
+
+ // internal function link, will point to noteInput of first FX, or to noteOutputFunc if no FX
+ void *doNoteOutputContext_;
+ void (*doNoteOutput_)(void *, MidiNoteGroup);
+ // // Static glue to link a pointer to a member function
+ // static void doNoteOutputForwarder(void *context, MidiNoteGroup note)
+ // {
+ // static_cast(context)->noteOutputFunc(note);
+ // }
+
+ static void midiFxSelNoteInputForwarder(void *context, midifx::MidiFXSelector *mfxSelector, uint8_t midiFXIndex, MidiNoteGroup note)
+ {
+ static_cast(context)->midiFxSelNoteInput(mfxSelector, midiFXIndex, note);
+ }
+
+ void midiFxSelNoteInput(midifx::MidiFXSelector *mfxSelector, uint8_t midiFXIndex, MidiNoteGroup note);
+ void reconnectInputsOutputs();
+};
+
+// static const u_int8_t kNumMidiFXGroups = 5;
+extern SubModeMidiFxGroup subModeMidiFx[NUM_MIDIFX_GROUPS];
diff --git a/Archive/OMX-27-firmware/src/modes/submodes/submode_potconfig.cpp b/Archive/OMX-27-firmware/src/modes/submodes/submode_potconfig.cpp
new file mode 100644
index 00000000..c70931bf
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/modes/submodes/submode_potconfig.cpp
@@ -0,0 +1,264 @@
+#include "submode_potconfig.h"
+#include "../../hardware/omx_disp.h"
+#include "../../hardware/omx_leds.h"
+#include "../../consts/colors.h"
+
+enum PotConfigPage
+{
+ POTPAGE_1,
+ POTPAGE_2,
+ POTPAGE_EXIT
+};
+
+SubModePotConfig::SubModePotConfig()
+{
+ params_.addPage(4);
+ params_.addPage(4);
+ params_.addPage(1); // Exit submode
+}
+
+void SubModePotConfig::onEnabled()
+{
+ params_.setSelPageAndParam(0, 0);
+ encoderSelect_ = true;
+ omxLeds.setDirty();
+ omxDisp.setDirty();
+
+ auxReleased_ = !midiSettings.keyState[0];
+}
+
+void SubModePotConfig::onDisabled()
+{
+ strip.clear();
+ omxLeds.setDirty();
+ omxDisp.setDirty();
+}
+
+void SubModePotConfig::loopUpdate()
+{
+}
+
+bool SubModePotConfig::updateLEDs()
+{
+ strip.clear();
+
+ // bool blinkState = omxLeds.getBlinkState();
+ // bool blinkStateSlow = omxLeds.getSlowBlinkState();
+
+ // Serial.println("MidiFX Leds");
+ // auto auxColor = midiFXParamView_ ? (blinkStateSlow ? ORANGE : LEDOFF) : RED;
+ strip.setPixelColor(0, RED);
+
+ // for(uint8_t i = 1; i < 26; i++)
+ // {
+ // strip.setPixelColor(i, LEDOFF);
+ // }
+
+ for (uint8_t i = 0; i < 5; i++)
+ {
+ auto bankColor = i == potSettings.potbank ? LTYELLOW : DKGREEN;
+ strip.setPixelColor(11 + i, bankColor);
+ }
+
+ // if (midiFXParamView_)
+ // {
+ // uint8_t selFXType = 0;
+
+ // if(getMidiFX(selectedMidiFX_) != nullptr)
+ // {
+ // // Serial.println("Selected MidiFX not null");
+ // selFXType = getMidiFX(selectedMidiFX_)->getFXType();
+ // }
+
+ // for (uint8_t i = 0; i < 8; i++)
+ // {
+ // auto fxColor = (i == selFXType ? GREEN : DKGREEN);
+
+ // strip.setPixelColor(19 + i, fxColor);
+ // }
+ // }
+
+ return true;
+}
+
+void SubModePotConfig::onEncoderChanged(Encoder::Update enc)
+{
+ SubmodeInterface::onEncoderChanged(enc);
+
+ // if (midiFXParamView_)
+ // {
+ // if (getMidiFX(selectedMidiFX_) != nullptr)
+ // {
+ // getMidiFX(selectedMidiFX_)->onEncoderChanged(enc);
+ // }
+ // }
+ // else
+ // {
+ // SubmodeInterface::onEncoderChanged(enc);
+ // }
+}
+
+void SubModePotConfig::onEncoderChangedEditParam(Encoder::Update enc)
+{
+ auto amt = enc.accel(2); // where 5 is the acceleration factor if you want it, 0 if you don't)
+
+ int8_t selPage = params_.getSelPage(); // Add one for readability
+ int8_t selParam = params_.getSelParam() + 1;
+
+ // PAGE ONE
+ if (selPage == POTPAGE_1)
+ {
+ int ccIndex = params_.getSelParam();
+
+ pots[potSettings.potbank][ccIndex] = constrain(pots[potSettings.potbank][ccIndex] + amt, 0, 127);
+ }
+ else if (selPage == POTPAGE_2)
+ {
+ if (selParam == 1)
+ {
+ pots[potSettings.potbank][4] = constrain(pots[potSettings.potbank][4] + amt, 0, 127);
+ }
+ else if (selParam == 4)
+ {
+ potSettings.potbank = constrain(potSettings.potbank + amt, 0, NUM_CC_BANKS - 1);
+ }
+ }
+
+ omxDisp.setDirty();
+ omxLeds.setDirty();
+}
+
+void SubModePotConfig::onEncoderButtonDown()
+{
+ if (params_.getSelPage() == POTPAGE_EXIT && params_.getSelParam() == 0)
+ {
+ setEnabled(false);
+ }
+ else
+ {
+ SubmodeInterface::onEncoderButtonDown();
+ }
+
+ omxDisp.setDirty();
+ omxLeds.setDirty();
+}
+
+bool SubModePotConfig::onKeyUpdate(OMXKeypadEvent e)
+{
+ int thisKey = e.key();
+ // auto keyState = midiSettings.keyState;
+
+ if (e.down())
+ {
+ if (thisKey == 0)
+ {
+ if (auxReleased_)
+ {
+ setEnabled(false);
+ }
+ }
+
+ // Quick Select FX Slot
+ if (thisKey >= 11 && thisKey <= 15)
+ {
+ potSettings.potbank = thisKey - 11;
+ }
+
+ // // Change FX type
+ // if (midiFXParamView_)
+ // {
+ // if (thisKey >= 19 && thisKey < 19 + 8)
+ // {
+ // changeMidiFXType(selectedMidiFX_, thisKey - 19);
+ // // selectMidiFX(thisKey - 19);
+ // }
+ // }
+ }
+
+ if (!e.down() && thisKey == 0)
+ {
+ // Used to prevent quickly exiting if entered through aux shortcut.
+ auxReleased_ = true;
+ }
+
+ omxDisp.setDirty();
+ omxLeds.setDirty();
+
+ return true;
+}
+
+void SubModePotConfig::setupPageLegends()
+{
+ omxDisp.clearLegends();
+
+ // omxDisp.dispPage = page + 1;
+
+ int8_t page = params_.getSelPage();
+
+ switch (page)
+ {
+ case POTPAGE_1:
+ {
+ omxDisp.legends[0] = "CC 1";
+ omxDisp.legends[1] = "CC 2";
+ omxDisp.legends[2] = "CC 3";
+ omxDisp.legends[3] = "CC 4";
+ omxDisp.legendVals[0] = pots[potSettings.potbank][0];
+ omxDisp.legendVals[1] = pots[potSettings.potbank][1];
+ omxDisp.legendVals[2] = pots[potSettings.potbank][2];
+ omxDisp.legendVals[3] = pots[potSettings.potbank][3];
+ }
+ break;
+ case POTPAGE_2:
+ {
+ omxDisp.legends[0] = "CC 5";
+ omxDisp.legends[1] = "";
+ omxDisp.legends[2] = "";
+ omxDisp.legends[3] = "PBNK";
+ omxDisp.legendVals[0] = pots[potSettings.potbank][4];
+ omxDisp.legendVals[1] = -127;
+ omxDisp.legendVals[2] = -127;
+ omxDisp.legendVals[3] = (potSettings.potbank + 1);
+ omxDisp.legendText[1] = "";
+ omxDisp.legendText[2] = "";
+ }
+ break;
+ case POTPAGE_EXIT:
+ {
+ omxDisp.legends[0] = "Exit";
+ omxDisp.legends[1] = "";
+ omxDisp.legends[2] = "";
+ omxDisp.legends[3] = "";
+ omxDisp.legendVals[0] = -127;
+ omxDisp.legendVals[1] = -127;
+ omxDisp.legendVals[2] = -127;
+ omxDisp.legendVals[3] = -127;
+ omxDisp.legendText[0] = "Exit";
+ omxDisp.legendText[1] = "";
+ omxDisp.legendText[2] = "";
+ omxDisp.legendText[3] = "";
+ }
+ break;
+ default:
+ break;
+ }
+}
+
+void SubModePotConfig::onDisplayUpdate()
+{
+ // omxLeds.updateBlinkStates();
+
+ // if (omxLeds.isDirty())
+ // {
+ // updateLEDs();
+ // }
+
+ if (omxDisp.isDirty())
+ {
+ if (!encoderConfig.enc_edit)
+ {
+ setupPageLegends();
+ omxDisp.dispGenericMode2(params_.getNumPages(), params_.getSelPage(), params_.getSelParam(), encoderSelect_);
+ }
+ }
+}
diff --git a/Archive/OMX-27-firmware/src/modes/submodes/submode_potconfig.h b/Archive/OMX-27-firmware/src/modes/submodes/submode_potconfig.h
new file mode 100644
index 00000000..73333721
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/modes/submodes/submode_potconfig.h
@@ -0,0 +1,32 @@
+#pragma once
+
+#include "submode_interface.h"
+
+// Holds a group of 4 midi fx slots.
+class SubModePotConfig : public SubmodeInterface
+{
+public:
+ // Constructor / deconstructor
+ SubModePotConfig();
+ ~SubModePotConfig() {}
+
+ // Interface methods
+ void loopUpdate() override;
+ bool updateLEDs() override;
+ void onEncoderChanged(Encoder::Update enc);
+ void onEncoderButtonDown() override;
+ bool onKeyUpdate(OMXKeypadEvent e) override;
+ void onDisplayUpdate() override;
+
+protected:
+ // Interface methods
+ void onEnabled() override;
+ void onDisabled() override;
+ void onEncoderChangedEditParam(Encoder::Update enc) override;
+
+private:
+ bool auxReleased_ = false; // set to aux state onEnable, must be true to exit mode with aux.
+ // int bankIndex = 0;
+
+ void setupPageLegends();
+};
diff --git a/Archive/OMX-27-firmware/src/modes/submodes/submode_preset.cpp b/Archive/OMX-27-firmware/src/modes/submodes/submode_preset.cpp
new file mode 100644
index 00000000..80d35d71
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/modes/submodes/submode_preset.cpp
@@ -0,0 +1,192 @@
+#include "submode_preset.h"
+#include "../../hardware/omx_disp.h"
+#include "../../hardware/omx_leds.h"
+#include "../../consts/colors.h"
+
+const char *saveLabel = "Save to";
+const char *loadLabel = "Load from";
+
+enum PotConfigPage
+{
+ POTPAGE_1,
+ POTPAGE_2,
+ POTPAGE_EXIT
+};
+
+SubModePreset::SubModePreset()
+{
+ params_.addPage(4);
+ params_.addPage(4);
+ params_.addPage(1); // Exit submode
+}
+
+void SubModePreset::onEnabled()
+{
+ params_.setSelPageAndParam(0, 0);
+ encoderSelect_ = true;
+ omxLeds.setDirty();
+ omxDisp.setDirty();
+
+ auxReleased_ = !midiSettings.keyState[0];
+}
+
+void SubModePreset::onDisabled()
+{
+ strip.clear();
+ omxLeds.setDirty();
+ omxDisp.setDirty();
+}
+
+void SubModePreset::configure(SubmodePresetMode mode, uint8_t selPreset, uint8_t numPresets, bool autoSave)
+{
+ if(selPreset >= numPresets || numPresets >= 16)
+ {
+ // Too many presets, or selPreset out of range
+ return;
+ }
+
+ this->mode = mode;
+ this->selPreset = selPreset;
+ this->numPresets = numPresets;
+ this->autoSave = autoSave;
+}
+
+void SubModePreset::setContextPtr(void *context)
+{
+ fptrContext_ = context;
+}
+void SubModePreset::setDoSaveFunc(void (*fptr)(void *, uint8_t))
+{
+ doSaveFptr_ = fptr;
+}
+void SubModePreset::setDoLoadFunc(void (*fptr)(void *, uint8_t))
+{
+ doLoadFptr_ = fptr;
+}
+
+void SubModePreset::doSave(uint8_t presetIndex)
+{
+ doSaveFptr_(fptrContext_, presetIndex);
+}
+
+void SubModePreset::doLoad(uint8_t presetIndex)
+{
+ doLoadFptr_(fptrContext_, presetIndex);
+}
+
+void SubModePreset::loopUpdate()
+{
+}
+
+bool SubModePreset::updateLEDs()
+{
+ strip.clear();
+
+ // bool blink = omxLeds.getBlinkState();
+ bool slowBlink = omxLeds.getSlowBlinkState();
+
+ // strip.setPixelColor(0, blink ? LTPURPLE : RED);
+
+ strip.setPixelColor(0, RED);
+
+
+ int keyColor = mode == PRESETMODE_LOAD ? BLUE : ORANGE;
+ int highlightColor = mode == PRESETMODE_LOAD ? LTCYAN : LTYELLOW;
+
+ for(uint8_t i = 11; i < 11 + numPresets; i++)
+ {
+ uint8_t presetIndex = i - 11;
+
+ int color = (slowBlink && presetIndex == selPreset) ? highlightColor : keyColor;
+
+ strip.setPixelColor(i, color);
+ }
+
+ return true;
+}
+
+void SubModePreset::onEncoderChanged(Encoder::Update enc)
+{
+ SubmodeInterface::onEncoderChanged(enc);
+}
+
+void SubModePreset::onEncoderChangedEditParam(Encoder::Update enc)
+{
+ omxDisp.setDirty();
+ omxLeds.setDirty();
+}
+
+void SubModePreset::onEncoderButtonDown()
+{
+ omxDisp.setDirty();
+ omxLeds.setDirty();
+}
+
+bool SubModePreset::onKeyUpdate(OMXKeypadEvent e)
+{
+ int thisKey = e.key();
+
+ if (e.down())
+ {
+ if (thisKey == 0)
+ {
+ // Aux key to cancel and go back
+ if (auxReleased_)
+ {
+ omxDisp.displayMessage(exitMsg);
+ setEnabled(false);
+ return true;
+ }
+ return true;
+ }
+
+ if (thisKey >= 11 && thisKey < 11 + numPresets)
+ {
+ uint8_t newPresetIndex = thisKey - 11;
+
+ if (mode == PRESETMODE_LOAD)
+ {
+ // Auto save current selected drum kit if loading a new one
+ if(newPresetIndex != selPreset && autoSave)
+ {
+ doSave(selPreset);
+ }
+ doLoad(newPresetIndex);
+ omxDisp.displayMessage("Loaded " + String(newPresetIndex + 1));
+ }
+ else if (mode == PRESETMODE_SAVE)
+ {
+ doSave(newPresetIndex);
+ omxDisp.displayMessage("Saved " + String(newPresetIndex + 1));
+ }
+ selPreset = thisKey - 11;
+ setEnabled(false);
+ return true;
+ }
+ }
+ // Key Up
+ else
+ {
+ if (thisKey == 0)
+ {
+ // Used to prevent quickly exiting if entered through aux shortcut.
+ auxReleased_ = true;
+ }
+ }
+
+ omxDisp.setDirty();
+ omxLeds.setDirty();
+
+ return true;
+}
+
+void SubModePreset::onDisplayUpdate()
+{
+ if (omxDisp.isDirty())
+ {
+ if (!encoderConfig.enc_edit)
+ {
+ omxDisp.dispGenericModeLabel(mode == PRESETMODE_LOAD ? loadLabel : saveLabel, 1, 0);
+ }
+ }
+}
diff --git a/Archive/OMX-27-firmware/src/modes/submodes/submode_preset.h b/Archive/OMX-27-firmware/src/modes/submodes/submode_preset.h
new file mode 100644
index 00000000..c5b6cdc2
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/modes/submodes/submode_preset.h
@@ -0,0 +1,60 @@
+#pragma once
+
+#include "submode_interface.h"
+
+enum SubmodePresetMode
+{
+ PRESETMODE_LOAD,
+ PRESETMODE_SAVE
+};
+
+// Submode for saving and loading a drum kit
+class SubModePreset : public SubmodeInterface
+{
+public:
+ // Constructor / deconstructor
+ SubModePreset();
+ ~SubModePreset() {}
+
+ void configure(SubmodePresetMode mode, uint8_t selPreset, uint8_t numPresets, bool autoSave);
+
+ void setContextPtr(void *context);
+ void setDoSaveFunc(void (*fptr)(void *, uint8_t));
+ void setDoLoadFunc(void (*fptr)(void *, uint8_t));
+
+ // Interface methods
+ void loopUpdate() override;
+ bool updateLEDs() override;
+ void onEncoderChanged(Encoder::Update enc);
+ void onEncoderButtonDown() override;
+ bool onKeyUpdate(OMXKeypadEvent e) override;
+ void onDisplayUpdate() override;
+
+ bool shouldBlockEncEdit() override { return true; }
+
+protected:
+ // Interface methods
+ void onEnabled() override;
+ void onDisabled() override;
+ void onEncoderChangedEditParam(Encoder::Update enc) override;
+
+private:
+
+
+ SubmodePresetMode mode;
+ uint8_t selPreset;
+ uint8_t numPresets;
+
+ void *fptrContext_;
+ void (*doSaveFptr_)(void *, uint8_t);
+ void (*doLoadFptr_)(void *, uint8_t);
+
+ bool autoSave;
+
+ bool auxReleased_ = false; // set to aux state onEnable, must be true to exit mode with aux.
+
+ void doSave(uint8_t presetIndex);
+ void doLoad(uint8_t presetIndex);
+
+ // void setupPageLegends();
+};
diff --git a/Archive/OMX-27-firmware/src/utils/PotPickupUtil.cpp b/Archive/OMX-27-firmware/src/utils/PotPickupUtil.cpp
new file mode 100644
index 00000000..093b8178
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/utils/PotPickupUtil.cpp
@@ -0,0 +1,53 @@
+#include "PotPickupUtil.h"
+
+void PotPickupUtil::SetVal(uint8_t newValue, bool midiIn)
+{
+ value = newValue;
+ if(midiIn)
+ {
+ revertValue = value;
+ }
+ directionDetermined = false;
+ pickedUp = false;
+}
+
+void PotPickupUtil::SaveRevertVal()
+{
+ revertValue = value;
+}
+
+void PotPickupUtil::RevertVal()
+{
+ value = revertValue;
+ directionDetermined = false;
+ pickedUp = false;
+}
+
+void PotPickupUtil::UpdatePot(uint8_t prevPot, uint8_t newPot)
+{
+ if (!directionDetermined)
+ {
+ directionCW = prevPot < value;
+ pickedUp = prevPot == value;
+ directionDetermined = true;
+ }
+
+ if (!pickedUp)
+ {
+ if (directionCW)
+ {
+ pickedUp = newPot >= value;
+ }
+ else
+ {
+ pickedUp = newPot <= value;
+ }
+ }
+
+ if (pickedUp)
+ {
+ value = newPot;
+ }
+
+ potValue = newPot;
+}
\ No newline at end of file
diff --git a/Archive/OMX-27-firmware/src/utils/PotPickupUtil.h b/Archive/OMX-27-firmware/src/utils/PotPickupUtil.h
new file mode 100644
index 00000000..a8bc6ed5
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/utils/PotPickupUtil.h
@@ -0,0 +1,23 @@
+#pragma once
+#include "../config.h"
+
+class PotPickupUtil
+{
+public:
+ uint8_t revertValue;
+ uint8_t value;
+ uint8_t potValue;
+
+ bool directionDetermined;
+ bool directionCW;
+ bool pickedUp;
+
+ // set midiIn true if value is coming from midi
+ void SetVal(uint8_t newValue, bool midiIn);
+ // saves current value to the revert value
+ void SaveRevertVal();
+ // Reverts the current value to the saved revert value
+ // Which gets saved from midiin or if SaveRevertVal() is called
+ void RevertVal();
+ void UpdatePot(uint8_t prevPot, uint8_t newPot);
+};
\ No newline at end of file
diff --git a/Archive/OMX-27-firmware/src/utils/RamMonitor.h b/Archive/OMX-27-firmware/src/utils/RamMonitor.h
new file mode 100644
index 00000000..9cc2a48a
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/utils/RamMonitor.h
@@ -0,0 +1,226 @@
+// Teensy 3.x RAM Monitor
+// copyright by Adrian Hunt (c) 2015 - 2016
+//
+// simplifies memory monitoring; providing both "raw"
+// memory information and with frequent calls to the
+// run() function, adjusted information with simulated
+// stack allocations. memory is also monitored for low
+// memory state and stack and heap crashes.
+//
+// raw memory information methods:
+//
+// int32_t unallocated() const;
+// calculates space between heap and current stack.
+// will return negitive if heap and stack currently
+// overlap, corruption is very likely.
+//
+// uint32_t stack_used() const;
+// calculates the current stack size.
+//
+// uint32_t heap_total() const;
+// return the heap size.
+//
+// uint32_t heap_used() const;
+// returns allocated heap.
+//
+// uint32_t heap_free() const;
+// returns unused heap.
+//
+// int32_t free() const;
+// calculates total free ram; unallocated and unused
+// heap. note that this uses the current stack size.
+//
+// uint32_t total() const;
+// returns total physical ram.
+//
+// extended memory information. These methods require
+// the RamMonitor object to be initialized and the
+// run() method called regularly.
+//
+// uint32_t stack_total() const;
+// returns the memory required for the stack. this
+// is determind by historical stack usage.
+//
+// int32_t stack_free() const;
+// returns stack space that can be used before the
+// stack grows and total size is increased.
+//
+// int32_t adj_unallocd() const;
+// calculates unallocated memory, reserving space
+// for the stack.
+//
+// int32_t adj_free() const;
+// calculates total free ram by using adjusted
+// unallocated and unused heap.
+//
+// bool warning_lowmem() const;
+// bool warning_crash() const;
+// return warning states: low memory is flagged when
+// adjusted unallocated memory is below a set value.
+// crash is flagged when reserved stack space over-
+// laps heap and there is a danger of corruption.
+//
+// void initialize();
+// initializes the RamMonitor object enabling stack
+// monitoring and the extended information methods.
+//
+// void run();
+// detects stack growth and updates memory warnings.
+// this function must be called regulary.
+//
+// when using the extended memory information methods,
+// a single RamMonitor object should be create at
+// global level. two static constants define values
+// that control stack allocation step size and the low
+// memory warning level. these values are in bytes.
+// the stack allocation step must be divisable by 4.
+//
+// static const uint16_t STACKALLOCATION;
+// static const uint16_t LOWMEM;
+//
+
+#ifndef RAMMONITOR_H
+#define RAMMONITOR_H "1.0"
+
+#include
+#include
+
+extern int *__brkval; // top of heap (dynamic ram): grows up towards stack
+extern char _estack; // bottom of stack, top of ram: stack grows down towards heap
+
+class RamMonitor
+{
+private:
+ typedef uint32_t MemMarker;
+ typedef uint8_t MemState;
+
+ // user defined consts
+ static const uint16_t STACKALLOCATION = 1024; // stack allocation step size: must be 32bit boundries, div'able by 4
+ static const uint16_t LOWMEM = 4096; // low memory warning: 4kb (less than between stack and heap)
+
+ // internal consts
+ static const uint32_t HWADDRESS_RAMSTART =
+#if defined(__MK20DX256__)
+ 0x1FFF8000; // teensy 3.1 (? 3.2 ?)
+#elif defined(__MKL26Z64__)
+ 0x ? ? ? ? ? ? ? ? ; // teensy LC
+#else
+ 0x1FFFE000; // teensy 3.0
+#endif
+ static const MemMarker MEMMARKER = 0x524D6D6D; // chars RMmm ... Ram Monitor memory marker
+ static const uint16_t MARKER_STEP = STACKALLOCATION / sizeof(MemMarker);
+
+ static const MemState msOk = 0;
+ static const MemState msLow = 1;
+ static const MemState msCrash = 2;
+
+ MemMarker *_mlastmarker; // last uncorrupted memory marker
+ MemState _mstate; // detected memory state
+
+ void _check_stack()
+ {
+ int32_t free;
+
+ // skip markers already comsumed by the stack
+ free = ((char *)&free) - ((char *)_mlastmarker);
+ if (free < 0)
+ {
+ int32_t steps;
+
+ steps = free / STACKALLOCATION; // note steps will be negitive
+ if (free % STACKALLOCATION)
+ --steps;
+
+ _mlastmarker += MARKER_STEP * steps;
+ };
+
+ // check last marker and move if corrupted
+ while ((*_mlastmarker != MEMMARKER) && (_mlastmarker >= (MemMarker *)__brkval))
+ _mlastmarker -= MARKER_STEP;
+ };
+
+public:
+ int32_t unallocated() const
+ {
+ char tos;
+ return &tos - (char *)__brkval;
+ }; // calcs space between heap and stack (current): will be negitive if heap/stack crash
+ uint32_t stack_used() const
+ {
+ char tos;
+ return &_estack - &tos;
+ }; // calcs stack size (current): grows into unallocated
+ uint32_t heap_total() const { return mallinfo().arena; }; // returns heap size: grows into unallocated
+ uint32_t heap_used() const { return mallinfo().uordblks; }; // returns heap allocated
+ uint32_t heap_free() const { return mallinfo().fordblks; }; // returns free heap
+
+ int32_t free() const { return unallocated() + heap_free(); }; // free ram: unallocated and unused heap
+ uint32_t total() const { return &_estack - (char *)HWADDRESS_RAMSTART; }; // physical ram
+
+ // these functions (along with initialize and run)
+ // create the ellusion of stack allocation.
+ uint32_t stack_total()
+ { // uses memory markers to "alloc" unallocated
+ _check_stack();
+ return &_estack - (char *)_mlastmarker;
+ };
+
+ int32_t stack_free()
+ { // calc stack usage before next marker corruption
+ char tos;
+
+ _check_stack();
+ return &tos - (char *)_mlastmarker;
+ };
+
+ int32_t adj_unallocd()
+ { // calcs space between heap and "alloc'd" stack: will be negitive if heap/stack crash
+ _check_stack();
+ return ((char *)_mlastmarker) - (char *)__brkval;
+ };
+
+ int32_t adj_free() { return adj_unallocd() + heap_free(); }; // free ram: unallocated and unused heap
+
+ bool warning_lowmem() const { return (_mstate & msLow); }; // returns true when unallocated memory is < LOWMEM
+ bool warning_crash() const { return (_mstate & msCrash); }; // returns true when stack is in danger of overwriting heap
+
+ void initialize()
+ {
+ MemMarker *marker = (MemMarker *)&_estack; // top of memory
+ int32_t size;
+ int32_t steps;
+
+ // skip current stack;
+ size = &_estack - (char *)▮ // current stack size: marker address is tos
+ steps = size / STACKALLOCATION;
+ if (size % STACKALLOCATION)
+ ++steps;
+
+ marker -= MARKER_STEP * steps;
+
+ // record current top of stack
+ _mlastmarker = marker;
+ _mstate = msOk;
+
+ // mark unused ram between top of stack and top of heap
+ while (marker >= (MemMarker *)__brkval)
+ {
+ *marker = MEMMARKER; // write memory marker
+ marker -= MARKER_STEP;
+ };
+ };
+
+ void run()
+ {
+ int32_t unallocd = adj_unallocd(); // calls _check_stack() internally
+
+ if (unallocd < 0)
+ _mstate = msCrash | msLow;
+ else if (unallocd < LOWMEM)
+ _mstate = msLow;
+ else
+ _mstate = msOk;
+ };
+};
+
+#endif
diff --git a/Archive/OMX-27-firmware/src/utils/chord_structs.h b/Archive/OMX-27-firmware/src/utils/chord_structs.h
new file mode 100644
index 00000000..63d31c98
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/utils/chord_structs.h
@@ -0,0 +1,241 @@
+#pragma once
+#include "../config.h"
+
+#define NUM_CHORD_PATTERNS 37
+
+extern const uint8_t kNumChordPatterns;
+extern const uint8_t kCustomChordPattern;
+
+// Last pattern is custom
+extern const int8_t chordPatterns[NUM_CHORD_PATTERNS - 1][3];
+
+extern const char *kChordMsg[NUM_CHORD_PATTERNS];
+
+#define NUM_CHORD_BALANCE 23
+
+extern const uint8_t kNumChordBalance;
+
+extern const int8_t chordBalance[NUM_CHORD_BALANCE][3];
+
+// extern int balSize;
+// extern int patSize;
+
+extern const char *kChordTypeDisp[2];
+extern const char *kVoicingNames[8];
+
+enum ChordVoicing
+{
+ CHRDVOICE_NONE,
+ CHRDVOICE_POWER,
+ CHRDVOICE_SUS2,
+ CHRDVOICE_SUS4,
+ CHRDVOICE_SUS24,
+ CHRDVOICE_ADD6,
+ CHRDVOICE_ADD69,
+ CHRDVOICE_KB11
+};
+
+enum ChordsModeParams
+{
+ CPARAM_UIMODE,
+ CPARAM_MAN_STRUM,
+ CPARAM_CHORD_TYPE,
+ CPARAM_CHORD_MFX,
+ CPARAM_CHORD_VEL,
+ CPARAM_CHORD_MCHAN,
+ CPARAM_BAS_NOTE,
+ CPARAM_BAS_OCT,
+ CPARAM_BAS_CHORD,
+ CPARAM_BAS_BALANCE,
+ CPARAM_INT_NUMNOTES,
+ CPARAM_INT_DEGREE,
+ CPARAM_INT_OCTAVE,
+ CPARAM_INT_TRANSPOSE,
+ CPARAM_INT_SPREAD,
+ CPARAM_INT_ROTATE,
+ CPARAM_INT_VOICING,
+ CPARAM_INT_SPRDUPDOWN,
+ CPARAM_INT_QUARTVOICE
+};
+
+struct CustomChordNote
+{
+ int8_t note : 7; // Root NoteNumber Offset or degree
+};
+
+struct ChordSettings
+{
+public:
+ int color = 0xFFFFFF;
+ uint8_t type : 1;
+ int8_t midiFx : 4;
+ uint8_t mchan : 4;
+ uint8_t velocity : 7;
+
+ // Basic Type:
+ uint8_t note : 4;
+ int8_t basicOct : 4;
+ uint8_t chord : 6;
+ uint8_t balance : 8; // 0 - 23 * 10
+
+ CustomChordNote customNotes[6];
+ // CustomChordDegree customDegrees[6];
+
+ // Interval Type:
+ uint8_t numNotes : 3;
+ uint8_t degree : 3; // degree from root note of scale, if scale is cmaj, degree of 0 = c, degree of 3 = e
+ int8_t octave : 4; // transposes note by octave
+ int8_t transpose : 5; // transposes note by semitone, can bump off scale
+ int8_t spread : 4; // spreads chord notes over octave
+ // spread 0 = C3,E3,G3 C3,E3,G3,B3
+ // spread -1 = C2,E3,G2 C2,E3,G2,B3 -1,*,-1 -1,*,-1,*
+ // spread -2 = C1,E3,G1 C1,E3,G1,B3 -2,*,-2 -2,*,-2,*
+ // spread 1 = C3,E4,G3 C3,E4,G3,B4 *,+1,* *,+1,*,+1
+ // spread 2 = C3,E5,G3 C3,E5,G3,B5 *,+2,* *,+2,*,+2
+ uint8_t rotate : 4; // Rotates the chord notes
+ // rotate 0 = C3,E3,G3 C3,E3,G3,B3
+ // rotate 1 = E3,G3,C4 E3,G3,B3,C4
+ // rotate 2 = G3,C4,E4 G3,B3,C4,E4
+ // rotate 3 = C3,E3,G3 B3,C4,E4,G4
+ // rotate 4 = E3,G3,C4 C3,E3,G3,B3
+ bool spreadUpDown = false; // spreads notes in both directions
+ // false = C3,E3,G3 C3,E3,G3,B3
+ // true = C2,E4,G2 C2,E4,G2,B4
+ // Spead -1 = C1,E4,G1 C1,E4,G1,E4
+ // bool widerInterDown = false; // Eh, not sure about this one. Could get with a rotate spread combo
+ // false = C3,E3,G3 C3,E3,G3,B3
+ // true = G2,C3,E3 C3,E3,G3,B3
+ bool quartalVoicing = false;
+ // false = C3,E3,G3 C3,E3,G3,B3
+ // true = C5,E3,G4 C5,E3,G4,B2
+ uint8_t voicing : 3;
+ // 0 = none - based off numNotes
+ // 1 = powerChord
+ // C3,G3 C3,G3,C4 C3,G3,C4
+ // 2 = sus2
+ // Shifts 2nd note down one degree
+ // C3,D3 C3,D3,G3 C3,D3,G3,B3
+ // 3 = sus4
+ // Shifts 2nd note up one degree
+ // C3,F3 C3,F3,G3 C3,F3,G3,B3
+ // 4 = sus2+4
+ // Shifts 2nd note down one degree and 3rd note down one degree
+ // C3,D3 C3,D3,F3 C3,D3,F3,B3
+ // 5 = add 6
+ // C3,D3,A3 C3,E3,G3,A3 C3,E3,G3,A3
+ // 6 = add 6 + 9
+ // C3,E3,A3,D4 C3,E3,G3,A3,D4 C3,E3,G3,A3,D4
+ // 7 = kennyBarron11
+ // Two hand jazz voicing
+ // 1,5,9, 10, 7th+oct,11+Oct
+ // C3,G3,D4,E4,B4,F5
+
+ ChordSettings()
+ {
+ type = 0;
+ midiFx = 0;
+ mchan = 0;
+ velocity = 100;
+
+ note = 0;
+ basicOct = 0;
+ chord = 0;
+ balance = 40; // Four note chord
+
+ numNotes = 3;
+ degree = 0;
+ octave = 0;
+ transpose = 0;
+ spread = 0;
+ rotate = 0;
+ spreadUpDown = false;
+ quartalVoicing = false;
+ voicing = 0;
+
+ for(uint8_t i = 0; i < 6; i++)
+ {
+ customNotes[i].note = 0;
+ }
+ }
+
+ void CopySettingsFrom(ChordSettings *other)
+ {
+ this->type = other->type;
+ this->midiFx = other->midiFx;
+ this->mchan = other->mchan;
+ this->velocity = other->velocity;
+
+ // Basic Type:
+ this->note = other->note;
+ this->basicOct = other->basicOct;
+ this->chord = other->chord;
+ this->balance = other->balance;
+
+ this->numNotes = other->numNotes;
+ this->degree = other->degree;
+ this->octave = other->octave;
+ this->transpose = other->transpose;
+ this->spread = other->spread;
+ this->rotate = other->rotate;
+ this->spreadUpDown = other->spreadUpDown;
+ this->quartalVoicing = other->quartalVoicing;
+ this->voicing = other->voicing;
+
+ for(uint8_t i = 0; i < 6; i++)
+ {
+ this->customNotes[i].note = other->customNotes[i].note;
+ }
+ }
+};
+
+struct ChordNotes
+{
+ bool active = false;
+ uint8_t channel = 0;
+ // uint8_t velocity = 100;
+ int notes[6] = {-1, -1, -1, -1, -1, -1};
+ uint8_t velocities[6] = {100, 100, 100, 100, 100, 100};
+ int8_t strumPos = 0;
+ int8_t encDelta = 0;
+ int8_t octIncrement = 0;
+ uint8_t midifx;
+ int rootNote;
+
+ void CopyFrom(ChordNotes *other)
+ {
+ active = other->active;
+ channel = other->channel;
+ for (uint8_t i = 0; i < 6; i++)
+ {
+ notes[i] = other->notes[i];
+ velocities[i] = other->velocities[i];
+ }
+ strumPos = other->strumPos;
+ encDelta = other->encDelta;
+ octIncrement = other->octIncrement;
+ midifx = other->midifx;
+ rootNote = other->rootNote;
+ }
+};
+
+struct ChordBalanceDetails
+{
+ int8_t type[4];
+ float velMult[4];
+
+ void Clear()
+ {
+ for(uint8_t i = 0; i < 4; i++)
+ {
+ type[i] = 0;
+ velMult[i] = 0;
+ }
+ }
+};
+
+enum ChordType
+{
+ CTYPE_BASIC, // Chords are copied from the Syntakt Chord machine, has a root, octave, scale, and ghosts. The ghosts determine number of notes in chord and notes will either be brought down or up and octave
+ CTYPE_INTERVAL, // Advanced chord config using intervals, can be locked to a the global scale.
+ CTYPE_BYOC, // Build your own chord however you'd like.
+};
diff --git a/Archive/OMX-27-firmware/src/utils/chord_util.cpp b/Archive/OMX-27-firmware/src/utils/chord_util.cpp
new file mode 100644
index 00000000..6cb516c0
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/utils/chord_util.cpp
@@ -0,0 +1,936 @@
+#include
+
+#include "chord_util.h"
+#include "../consts/consts.h"
+#include "../midi/midi.h"
+#include "../consts/colors.h"
+#include "../hardware/omx_leds.h"
+#include "../hardware/omx_disp.h"
+#include "../midi/noteoffs.h"
+#include "../modes/sequencer.h"
+
+const uint8_t kNumChordPatterns = 37;
+const uint8_t kCustomChordPattern = kNumChordPatterns - 1;
+
+// Last pattern is custom
+const int8_t chordPatterns[kNumChordPatterns - 1][3] = {
+ {4, 7, -1}, // Major C E G
+ {3, 7, -1}, // minor C Eb G
+ {2, 7, -1}, // sus2 C D G
+ {5, 7, -1}, // sus4 C F G
+ {3, 6, -1}, // mb5 C Eb Gb
+ {4, 6, -1}, // Mb5 C E Gb
+ {4, 8, -1}, // M#5 C E G#
+ {4, 14, -1}, // M9no5 C E D2 no 5
+
+ {3, 6, 9}, // dim7 C Eb Gb A
+ {3, 6, 10}, // m7b5 C Eb Gb Bb
+ {3, 7, 8}, // mb6 C Eb G Ab
+ {3, 7, 9}, // m6 C Eb G A
+ {3, 7, 10}, // m7 C Eb G Bb
+ {3, 7, 11}, // mMaj7 C Eb G B
+ {3, 7, 14}, // madd9 C Eb G D
+ {3, 8, 10}, // m7#5 C Eb Ab Bb
+ {3, 10, 13}, // m7b9no5 C Eb Bb Db2
+ {3, 10, 14}, // m9no5 C Eb Bb D2
+
+ {4, 5, 9}, // M6add4no5 C E F A
+ {4, 6, 10}, // M7b5 C E Gb Bb
+ {4, 6, 11}, // Maj7b5 C E Gb B
+ {4, 6, 14}, // Madd9b5 C E Gb D2
+ {4, 7, 8}, // Maddb5 C E G Gb
+ {4, 7, 9}, // M6 C E G A
+ {4, 7, 10}, // M7 C E G Bb
+ {4, 7, 11}, // Maj7 C E G B
+ {4, 7, 14}, // Madd9 C E G D2
+ {4, 8, 10}, // M7#5 C E G# Bb
+ {4, 10, 13}, // M7b9no5 C E Bb Db2
+ {4, 11, 14}, // Maj9no5 C E B D2
+ {4, 11, 21}, // Maj7/6no5 C E B A2
+ {5, 7, 8}, // sus4add#5 C F G G#
+ {5, 7, 10}, // 7sus4 C F G Bb
+ {5, 8, 13}, // sus4#5b9 C F G# Db2
+ {5, -1, -1}, // Fourth CF
+ {7, -1, -1} // Fifth CG
+};
+
+const char *kChordMsg[kNumChordPatterns] = {
+ "Major",
+ "Minor",
+ "sus2",
+ "sus4",
+ "mb5",
+ "Mb5",
+ "M#5",
+ "M9no5",
+
+ "dim7",
+ "m7b5",
+ "mb6",
+ "m6",
+ "m7",
+ "mMaj7",
+ "madd9",
+ "m7#5",
+ "m7b9no5",
+ "m9no5",
+
+ "M6add4no5",
+ "M7b5",
+ "Maj7b5",
+ "Madd9b5",
+ "Maddb5",
+ "M6",
+ "M7",
+ "Maj7",
+ "Madd9",
+ "M7#5",
+ "M7b9no5",
+ "Maj9no5",
+ "Maj7/6no5",
+ "sus4add#5",
+ "7sus4",
+ "sus4#5b9",
+
+ "Fourths",
+ "Fifth",
+ "Custom"};
+
+const uint8_t kNumChordBalance = 23;
+
+const int8_t chordBalance[kNumChordBalance][3] = {
+ {-10, -10, -10}, // 0 Single Note - 0
+ {0, -10, -10}, // 10 Power Chord - 10
+ {0, 0, -10}, // 20 Triad
+ {0, 0, 0}, // 30 Four notes - Root
+ {0, 0, 0}, // 32 Four notes - Root
+ {-10, 0, 0}, // 37
+ {-1, 0, 0}, // 42
+ {-1, -10, 0}, // 47
+ {-1, -1, 0}, // 52
+ {-1, -1, -10}, // 57
+ {-1, -1, -1}, // 62 - Inv 1
+ {-10, -1, -1}, // 69
+ {0, -1, -1}, // 74 - Inv 2
+ {0, -10, -1}, // 79
+ {0, 0, -1}, // 84 - Inv 3
+ {0, 0, -10}, // 91
+ {0, 0, 0}, // 96
+ {-10, 0, 0}, // 101
+ {1, 0, 0}, // 106
+ {1, -10, 0}, // 111
+ {1, 1, 0}, // 116
+ {1, 1, -10}, // 121
+ {1, 1, 1}, // 127
+};
+
+// int balSize = sizeof(chordBalance);
+// int patSize = sizeof(chordPatterns);
+
+const char *kChordTypeDisp[2] = {"BASC", "INTV"};
+const char *kVoicingNames[8] = {"NONE", "POWR", "SUS2", "SUS4", "SU24", "+6", "+6+9", "KB11"};
+
+// extern const uint8_t kNumChordPatterns = 37;
+// extern const uint8_t kCustomChordPattern = kNumChordPatterns - 1;
+
+// // Last pattern is custom
+// extern const int8_t chordPatterns[kNumChordPatterns - 1][3] = {
+// {4, 7, -1}, // Major C E G
+// {3, 7, -1}, // minor C Eb G
+// {2, 7, -1}, // sus2 C D G
+// {5, 7, -1}, // sus4 C F G
+// {3, 6, -1}, // mb5 C Eb Gb
+// {4, 6, -1}, // Mb5 C E Gb
+// {4, 8, -1}, // M#5 C E G#
+// {4, 14, -1}, // M9no5 C E D2 no 5
+
+// {3, 6, 9}, // dim7 C Eb Gb A
+// {3, 6, 10}, // m7b5 C Eb Gb Bb
+// {3, 7, 8}, // mb6 C Eb G Ab
+// {3, 7, 9}, // m6 C Eb G A
+// {3, 7, 10}, // m7 C Eb G Bb
+// {3, 7, 11}, // mMaj7 C Eb G B
+// {3, 7, 14}, // madd9 C Eb G D
+// {3, 8, 10}, // m7#5 C Eb Ab Bb
+// {3, 10, 13}, // m7b9no5 C Eb Bb Db2
+// {3, 10, 14}, // m9no5 C Eb Bb D2
+
+// {4, 5, 9}, // M6add4no5 C E F A
+// {4, 6, 10}, // M7b5 C E Gb Bb
+// {4, 6, 11}, // Maj7b5 C E Gb B
+// {4, 6, 14}, // Madd9b5 C E Gb D2
+// {4, 7, 8}, // Maddb5 C E G Gb
+// {4, 7, 9}, // M6 C E G A
+// {4, 7, 10}, // M7 C E G Bb
+// {4, 7, 11}, // Maj7 C E G B
+// {4, 7, 14}, // Madd9 C E G D2
+// {4, 8, 10}, // M7#5 C E G# Bb
+// {4, 10, 13}, // M7b9no5 C E Bb Db2
+// {4, 11, 14}, // Maj9no5 C E B D2
+// {4, 11, 21}, // Maj7/6no5 C E B A2
+// {5, 7, 8}, // sus4add#5 C F G G#
+// {5, 7, 10}, // 7sus4 C F G Bb
+// {5, 8, 13}, // sus4#5b9 C F G# Db2
+// {5, -1, -1}, // Fourth CF
+// {7, -1, -1} // Fifth CG
+// };
+
+// extern const char *kChordMsg[kNumChordPatterns] = {
+// "Major",
+// "Minor",
+// "sus2",
+// "sus4",
+// "mb5",
+// "Mb5",
+// "M#5",
+// "M9no5",
+
+// "dim7",
+// "m7b5",
+// "mb6",
+// "m6",
+// "m7",
+// "mMaj7",
+// "madd9",
+// "m7#5",
+// "m7b9no5",
+// "m9no5",
+
+// "M6add4no5",
+// "M7b5",
+// "Maj7b5",
+// "Madd9b5",
+// "Maddb5",
+// "M6",
+// "M7",
+// "Maj7",
+// "Madd9",
+// "M7#5",
+// "M7b9no5",
+// "Maj9no5",
+// "Maj7/6no5",
+// "sus4add#5",
+// "7sus4",
+// "sus4#5b9",
+
+// "Fourths",
+// "Fifth",
+// "Custom"};
+
+// extern const uint8_t kNumChordBalance = 23;
+
+// extern const int8_t chordBalance[kNumChordBalance][3] = {
+// {-10, -10, -10}, // 0 Single Note - 0
+// {0, -10, -10}, // 10 Power Chord - 10
+// {0, 0, -10}, // 20 Triad
+// {0, 0, 0}, // 30 Four notes - Root
+// {0, 0, 0}, // 32 Four notes - Root
+// {-10, 0, 0}, // 37
+// {-1, 0, 0}, // 42
+// {-1, -10, 0}, // 47
+// {-1, -1, 0}, // 52
+// {-1, -1, -10}, // 57
+// {-1, -1, -1}, // 62 - Inv 1
+// {-10, -1, -1}, // 69
+// {0, -1, -1}, // 74 - Inv 2
+// {0, -10, -1}, // 79
+// {0, 0, -1}, // 84 - Inv 3
+// {0, 0, -10}, // 91
+// {0, 0, 0}, // 96
+// {-10, 0, 0}, // 101
+// {1, 0, 0}, // 106
+// {1, -10, 0}, // 111
+// {1, 1, 0}, // 116
+// {1, 1, -10}, // 121
+// {1, 1, 1}, // 127
+// };
+
+// extern int balSize = sizeof(chordBalance);
+// extern int patSize = sizeof(chordPatterns);
+
+// extern const char *kChordTypeDisp[8] = {"BASC", "INTV"};
+// extern const char *kVoicingNames[8] = {"NONE", "POWR", "SUS2", "SUS4", "SU24", "+6", "+6+9", "KB11"};
+
+ChordUtil::ChordUtil()
+{
+ musicScale_.calculateScale(0,0);
+}
+
+int ChordUtil::AddOctave(int note, int8_t octave)
+{
+ if (note < 0 || note > 127)
+ return -1;
+
+ int newNote = note + (12 * octave);
+ if (newNote < 0 || newNote > 127)
+ return -1;
+ return newNote;
+}
+
+int ChordUtil::TransposeNote(int note, int8_t semitones)
+{
+ if (note < 0 || note > 127)
+ return -1;
+
+ int newNote = note + semitones;
+ if (newNote < 0 || newNote > 127)
+ return -1;
+ return newNote;
+}
+
+bool ChordUtil::constructChord(ChordSettings *chord, ChordNotes *chordNotes, int8_t autoOctave, int scaleRoot, int scalePattern, bool midiFx)
+{
+ // Serial.println("Constructing Chord: " + String(chordIndex));
+ // auto chord = chords_[chordIndex];
+
+ if (chord->type == CTYPE_BASIC)
+ {
+ return constructChordBasic(chord, chordNotes, autoOctave, midiFx);
+ }
+ else if (chord->type == CTYPE_INTERVAL)
+ {
+ return constructChordInterval(chord, chordNotes, autoOctave, scaleRoot, scalePattern, midiFx);
+ }
+
+ return constructChordBasic(chord, chordNotes, autoOctave, midiFx);
+}
+
+bool ChordUtil::constructChordInterval(ChordSettings *chord, ChordNotes *chordNotes, int8_t autoOctave, int scaleRoot, int scalePattern, bool midiFx)
+{
+ musicScale_.calculateScaleIfModified(scaleRoot, scalePattern);
+
+ // int8_t octave = midiSettings.octave + chord->octave;
+
+ int8_t octave = midiFx ? autoOctave + chord->octave : midiSettings.octave + chord->octave;
+
+ uint8_t numNotes = 0;
+
+ for (uint8_t i = 0; i < 6; i++)
+ {
+ chordNotes->notes[i] = -1;
+ chordNotes->velocities[i] = chord->velocity;
+ }
+
+ if (chord->numNotes == 0)
+ {
+ return false;
+ }
+ else if (chord->numNotes == 1)
+ {
+ chordNotes->notes[0] = musicScale_.getNoteByDegree(chord->degree, octave);
+ numNotes = 1;
+ }
+ else if (chord->numNotes == 2)
+ {
+ chordNotes->notes[0] = musicScale_.getNoteByDegree(chord->degree, octave);
+ chordNotes->notes[1] = musicScale_.getNoteByDegree(chord->degree + 2, octave);
+ numNotes = 2;
+ }
+ else if (chord->numNotes == 3)
+ {
+ chordNotes->notes[0] = musicScale_.getNoteByDegree(chord->degree, octave);
+ chordNotes->notes[1] = musicScale_.getNoteByDegree(chord->degree + 2, octave);
+ chordNotes->notes[2] = musicScale_.getNoteByDegree(chord->degree + 4, octave);
+ numNotes = 3;
+ }
+ else if (chord->numNotes == 4)
+ {
+ chordNotes->notes[0] = musicScale_.getNoteByDegree(chord->degree, octave);
+ chordNotes->notes[1] = musicScale_.getNoteByDegree(chord->degree + 2, octave);
+ chordNotes->notes[2] = musicScale_.getNoteByDegree(chord->degree + 4, octave);
+ chordNotes->notes[3] = musicScale_.getNoteByDegree(chord->degree + 6, octave);
+ numNotes = 4;
+ }
+
+ chordNotes->rootNote = chordNotes->notes[0];
+
+ // Serial.println("numNotes: " + String(numNotes));
+
+ switch (chord->voicing)
+ {
+ case CHRDVOICE_NONE:
+ {
+ }
+ break;
+ case CHRDVOICE_POWER:
+ {
+ if (chord->numNotes > 1)
+ {
+ chordNotes->notes[1] = musicScale_.getNoteByDegree(chord->degree + 4, octave);
+ }
+ if (chord->numNotes > 2)
+ {
+ chordNotes->notes[2] = chordNotes->notes[1] + 12;
+ for (uint8_t i = 3; i < 6; i++)
+ {
+ chordNotes->notes[i] = -1;
+ }
+ numNotes = 3;
+ }
+ }
+ break;
+ case CHRDVOICE_SUS2:
+ {
+ if (chord->numNotes > 1)
+ {
+ chordNotes->notes[1] = musicScale_.getNoteByDegree(chord->degree + 1, octave);
+ }
+ }
+ break;
+ case CHRDVOICE_SUS4:
+ {
+ if (chord->numNotes > 1)
+ {
+ chordNotes->notes[1] = musicScale_.getNoteByDegree(chord->degree + 3, octave);
+ }
+ }
+ break;
+ case CHRDVOICE_SUS24:
+ {
+ if (chord->numNotes > 1)
+ {
+ chordNotes->notes[1] = musicScale_.getNoteByDegree(chord->degree + 1, octave);
+ }
+ if (chord->numNotes > 2)
+ {
+ chordNotes->notes[2] = musicScale_.getNoteByDegree(chord->degree + 3, octave);
+ }
+ }
+ break;
+ case CHRDVOICE_ADD6:
+ {
+ chordNotes->notes[chord->numNotes] = musicScale_.getNoteByDegree(chord->degree + 5, octave);
+ numNotes = chord->numNotes + 1;
+ }
+ break;
+ case CHRDVOICE_ADD69:
+ {
+ chordNotes->notes[chord->numNotes] = musicScale_.getNoteByDegree(chord->degree + 5, octave);
+ chordNotes->notes[chord->numNotes + 1] = musicScale_.getNoteByDegree(chord->degree + 8, octave);
+ numNotes = chord->numNotes + 2;
+ }
+ break;
+ case CHRDVOICE_KB11:
+ {
+ if (chord->numNotes > 1)
+ {
+ chordNotes->notes[0] = musicScale_.getNoteByDegree(chord->degree + 0, octave);
+ chordNotes->notes[1] = musicScale_.getNoteByDegree(chord->degree + 4, octave);
+ numNotes = 2;
+ }
+ if (chord->numNotes > 2)
+ {
+ chordNotes->notes[2] = musicScale_.getNoteByDegree(chord->degree + 8, octave);
+ numNotes = 3;
+ }
+ if (chord->numNotes > 3)
+ {
+ chordNotes->notes[3] = musicScale_.getNoteByDegree(chord->degree + 9, octave);
+ chordNotes->notes[4] = musicScale_.getNoteByDegree(chord->degree + 6, octave + 1);
+ chordNotes->notes[5] = musicScale_.getNoteByDegree(chord->degree + 10, octave + 1);
+ numNotes = 6;
+ }
+ }
+ break;
+ default:
+ break;
+ }
+
+ // Serial.println("numNotes: " + String(numNotes));
+
+ if (chord->quartalVoicing)
+ {
+ chordNotes->notes[0] = AddOctave(chordNotes->notes[0], 2);
+ chordNotes->notes[1] = AddOctave(chordNotes->notes[1], 0);
+ chordNotes->notes[2] = AddOctave(chordNotes->notes[2], 1);
+ chordNotes->notes[3] = AddOctave(chordNotes->notes[3], -1);
+ }
+
+ if (chord->spreadUpDown)
+ {
+ for (uint8_t i = 0; i < 6; i++)
+ {
+ if (i % 2 == 0)
+ {
+ chordNotes->notes[i] = AddOctave(chordNotes->notes[i], -1);
+ }
+ else
+ {
+ chordNotes->notes[i] = AddOctave(chordNotes->notes[i], 1);
+ }
+ }
+ }
+
+ if (chord->spread < 0)
+ {
+ for (uint8_t i = 0; i < 6; i++)
+ {
+ if (i % 2 == 0)
+ {
+ chordNotes->notes[i] = AddOctave(chordNotes->notes[i], chord->spread);
+ }
+ }
+ }
+ else if (chord->spread > 0)
+ {
+ for (uint8_t i = 0; i < 6; i++)
+ {
+ if (i % 2 != 0)
+ {
+ chordNotes->notes[i] = AddOctave(chordNotes->notes[i], chord->spread);
+ }
+ }
+ }
+
+ if (chord->rotate != 0 && numNotes > 0)
+ {
+ int temp[numNotes];
+
+ uint8_t val = numNotes - chord->rotate;
+
+ uint8_t offset = chord->rotate % numNotes;
+
+ for (uint8_t i = 0; i < offset; i++)
+ {
+ chordNotes->notes[i] = AddOctave(chordNotes->notes[i], 1);
+ }
+
+ for (uint8_t i = 0; i < numNotes; i++)
+ {
+ temp[i] = chordNotes->notes[abs((i + val) % numNotes)];
+ }
+ for (int i = 0; i < numNotes; i++)
+ {
+ chordNotes->notes[i] = temp[i];
+ }
+ }
+
+ for (uint8_t i = 0; i < 6; i++)
+ {
+ chordNotes->notes[i] = TransposeNote(chordNotes->notes[i], chord->transpose);
+ }
+
+ chordNotes->midifx = chord->midiFx;
+
+ return true;
+}
+
+bool ChordUtil::constructChordBasic(ChordSettings * chord, ChordNotes * chordNotes, int8_t autoOctave, bool midiFx)
+{
+ // auto chord = chords_[chordIndex];
+
+ // int8_t octave = midiSettings.octave + chord->octave;
+
+ // uint8_t numNotes = 0;
+
+ for (uint8_t i = 0; i < 6; i++)
+ {
+ chordNotes->notes[i] = -1;
+ // Note velocity is set below by the chord balance
+ }
+
+ // int adjRoot = notes[thisKey] + (midiSettings.octave + 1 * 12);
+
+ int8_t octave = midiFx ? autoOctave + chord->basicOct : chord->basicOct;
+
+ int rootNote = chord->note + ((octave + 5) * 12);
+
+ if (rootNote < 0 || rootNote > 127)
+ return false;
+
+ chordNotes->rootNote = rootNote;
+
+ chordNotes->midifx = chord->midiFx;
+
+ chordNotes->notes[0] = rootNote;
+
+ if (chord->chord == kCustomChordPattern)
+ {
+ for (uint8_t i = 0; i < 6; i++)
+ {
+ int noteOffset = chord->customNotes[i].note;
+
+ if (noteOffset != 0 || (noteOffset == 0 && i == 0))
+ {
+ chordNotes->notes[i] = rootNote + noteOffset;
+ }
+ // else offset is zero, do nothing.
+ }
+ }
+ else
+ {
+ auto pattern = chordPatterns[chord->chord];
+
+ for (uint8_t i = 0; i < 3; i++)
+ {
+ if (pattern[i] >= 0)
+ {
+ chordNotes->notes[i + 1] = rootNote + pattern[i];
+ }
+ }
+ }
+
+ updateChordBalance(chord->balance);
+
+ for (uint8_t i = 0; i < 4; i++)
+ {
+ int pnote = chordNotes->notes[i];
+
+ if (pnote >= 0 && pnote <= 127)
+ {
+ int bal = chordBalanceDetails.type[i];
+
+ chordNotes->notes[i] = (bal <= -10 ? -1 : (pnote + (12 * bal)));
+ chordNotes->velocities[i] = chord->velocity * chordBalanceDetails.velMult[i];
+ }
+ }
+
+ return true;
+}
+
+ChordBalanceDetails ChordUtil::getChordBalance(uint8_t balance)
+{
+ updateChordBalance(balance);
+ return chordBalanceDetails;
+}
+
+MusicScales* ChordUtil::getMusicScale()
+{
+ return &musicScale_;
+}
+
+void ChordUtil::updateChordBalance(uint8_t balance)
+{
+ // ChordBalanceDetails bDetails;
+
+ chordBalanceDetails.type[0] = 0;
+ chordBalanceDetails.velMult[0] = 1.0f;
+
+ uint8_t balanceIndex = balance / 10;
+
+ auto balancePat = chordBalance[balanceIndex];
+
+ for (uint8_t i = 0; i < 3; i++)
+ {
+ int8_t bal = balancePat[i];
+
+ chordBalanceDetails.type[i + 1] = bal;
+
+ if (balanceIndex < kNumChordBalance)
+ {
+ int8_t nextBal = chordBalance[balanceIndex + 1][i];
+
+ if ((balance % 10) != 0)
+ {
+ if (nextBal > -10)
+ {
+ chordBalanceDetails.type[i + 1] = nextBal;
+ }
+ }
+
+ float v1 = bal <= -10 ? 0.0f : 1.0f;
+ float v2 = nextBal <= -10 ? 0.0f : 1.0f;
+
+ chordBalanceDetails.velMult[i + 1] = map((float)balance, balanceIndex * 10.0f, (balanceIndex + 1) * 10.0f, v1, v2);
+ }
+ else
+ {
+ chordBalanceDetails.velMult[i + 1] = 1.0f;
+ }
+ }
+}
+
+void ChordUtil::onEncoderChangedEditParam(Encoder::Update *enc, ChordSettings *chord, uint8_t selectedParmIndex, uint8_t targetParamIndex, uint8_t paramType)
+{
+ if (selectedParmIndex != targetParamIndex)
+ return;
+
+ auto amtSlow = enc->accel(1);
+ auto amtFast = enc->accel(5);
+
+ // bool triggerChord = false;
+
+ switch (paramType)
+ {
+ // Handled by Chord Mode class
+ case CPARAM_UIMODE:
+ {
+ // uiMode_ = constrain(uiMode_ + amtSlow, 0, 1);
+ // if (amtSlow != 0)
+ // {
+ // allNotesOff();
+ // // omxUtil.allOff();
+ // }
+ }
+ break;
+ // Handled by Chord Mode class
+ case CPARAM_MAN_STRUM:
+ {
+ // if (mode_ == CHRDMODE_MANSTRUM)
+ // {
+ // if (enc->dir() < 0)
+ // {
+ // mode_ = CHRDMODE_PLAY;
+ // }
+ // }
+ // else
+ // {
+ // if (enc->dir() > 0)
+ // {
+ // mode_ = CHRDMODE_MANSTRUM;
+ // }
+ // }
+ }
+ break;
+ case CPARAM_CHORD_TYPE:
+ {
+ // if (amtSlow != 0)
+ // {
+ // if (chordEditMode_)
+ // {
+ // onChordEditOff();
+ // enterChordEditMode();
+ // }
+ // else
+ // {
+ // onChordOff(selectedChord_);
+ // }
+ // }
+
+ chord->type = constrain(chord->type + amtSlow, 0, 1);
+ }
+ break;
+ // Handled by Chord Mode class
+ case CPARAM_CHORD_MFX:
+ {
+ // int8_t newMidiFx = constrain(chord->midiFx + amtSlow, -1, NUM_MIDIFX_GROUPS - 1);
+ // chord->midiFx = newMidiFx;
+ }
+ break;
+ case CPARAM_CHORD_VEL:
+ {
+ chord->velocity = constrain(chord->velocity + amtFast, 0, 127);
+ }
+ break;
+ case CPARAM_CHORD_MCHAN:
+ {
+ chord->mchan = constrain(chord->mchan + amtSlow, 0, 15);
+ }
+ break;
+ case CPARAM_BAS_NOTE:
+ {
+ chord->note = constrain(chord->note + amtSlow, 0, 11);
+ // triggerChord = amtSlow != 0;
+ }
+ break;
+ case CPARAM_BAS_OCT:
+ {
+ chord->basicOct = constrain(chord->basicOct + amtSlow, -5, 4);
+ // triggerChord = amtSlow != 0;
+ }
+ break;
+ case CPARAM_BAS_CHORD:
+ {
+ // uint8_t prevChord = chord->chord;
+ chord->chord = constrain(chord->chord + amtSlow, 0, kNumChordPatterns - 1);
+ // if (chord->chord != prevChord)
+ // {
+ // // triggerChord = true;
+
+ // // constructChord(selectedChord_);
+ // // omxDisp.displayMessage(kChordMsg[chord->chord]);
+ // }
+ }
+ break;
+ case CPARAM_BAS_BALANCE:
+ {
+ chord->balance = constrain(chord->balance + amtFast, 0, (kNumChordBalance - 1) * 10);
+ updateChordBalance(chord->balance);
+
+ // omxDisp.chordBalanceMsg(activeChordBalance_.type, activeChordBalance_.velMult, 10);
+
+ // if (amtSlow != 0) // To see notes change on keyboard leds
+ // {
+ // constructChord(selectedChord_);
+ // }
+ }
+ break;
+ case CPARAM_INT_NUMNOTES:
+ {
+ chord->numNotes = constrain(chord->numNotes + amtSlow, 1, 4);
+ }
+ break;
+ case CPARAM_INT_DEGREE:
+ {
+ chord->degree = constrain(chord->degree + amtSlow, 0, 7);
+ }
+ break;
+ case CPARAM_INT_OCTAVE:
+ {
+ chord->octave = constrain(chord->octave + amtSlow, -2, 2);
+ }
+ break;
+ case CPARAM_INT_TRANSPOSE:
+ {
+ chord->transpose = constrain(chord->transpose + amtSlow, -7, 7);
+ }
+ break;
+ case CPARAM_INT_SPREAD:
+ {
+ chord->spread = constrain(chord->spread + amtSlow, -2, 2);
+ }
+ break;
+ case CPARAM_INT_ROTATE:
+ {
+ chord->rotate = constrain(chord->rotate + amtSlow, 0, 4);
+ }
+ break;
+ case CPARAM_INT_VOICING:
+ {
+ chord->voicing = constrain(chord->voicing + amtSlow, 0, 7);
+ }
+ break;
+ case CPARAM_INT_SPRDUPDOWN:
+ {
+ chord->spreadUpDown = constrain(chord->spreadUpDown + amtSlow, 0, 1);
+ }
+ break;
+ case CPARAM_INT_QUARTVOICE:
+ {
+ chord->quartalVoicing = constrain(chord->quartalVoicing + amtSlow, 0, 1);
+ }
+ break;
+ }
+}
+
+void ChordUtil::setupPageLegend(ChordSettings *chord, uint8_t index, uint8_t paramType)
+ {
+ switch (paramType)
+ {
+ // Handled by Chord Mode class
+ case CPARAM_UIMODE:
+ {
+ omxDisp.legends[index] = "UI";
+ // omxDisp.legendText[index] = kUIModeDisp[uiMode_];
+ }
+ break;
+ // Handled by Chord Mode class
+ case CPARAM_MAN_STRUM:
+ {
+ omxDisp.legends[index] = "STRUM";
+ // omxDisp.legendText[index] = mode_ == CHRDMODE_MANSTRUM ? "ON" : "OFF";
+ }
+ break;
+ case CPARAM_CHORD_TYPE:
+ {
+ omxDisp.legends[index] = "TYPE";
+ omxDisp.legendText[index] = kChordTypeDisp[chord->type];
+ }
+ break;
+ case CPARAM_CHORD_MFX:
+ {
+ omxDisp.legends[index] = "MIFX";
+ if (chord->midiFx >= 0)
+ {
+ omxDisp.legendVals[index] = chord->midiFx + 1;
+ }
+ else
+ {
+ omxDisp.legendText[index] = "OFF";
+ }
+ }
+ break;
+ case CPARAM_CHORD_VEL:
+ {
+ omxDisp.legends[index] = "VEL";
+ omxDisp.legendVals[index] = chord->velocity;
+ }
+ break;
+ case CPARAM_CHORD_MCHAN:
+ {
+ omxDisp.legends[index] = "MCHAN";
+ omxDisp.legendVals[index] = chord->mchan + 1;
+ }
+ break;
+ case CPARAM_BAS_NOTE:
+ {
+ omxDisp.legends[index] = "NOTE";
+ omxDisp.legendText[index] = MusicScales::getNoteName(chord->note);
+ }
+ break;
+ case CPARAM_BAS_OCT:
+ {
+ omxDisp.legends[index] = "C-OCT";
+ omxDisp.legendVals[index] = chord->basicOct + 4;
+ }
+ break;
+ case CPARAM_BAS_CHORD:
+ {
+ omxDisp.legends[index] = "CHRD";
+ omxDisp.legendVals[index] = chord->chord;
+ }
+ break;
+ case CPARAM_BAS_BALANCE:
+ {
+ omxDisp.legends[index] = "BAL";
+ omxDisp.legendVals[index] = map(chord->balance, 0, (kNumChordBalance - 1) * 10, 0, 127);
+ }
+ break;
+ case CPARAM_INT_NUMNOTES:
+ {
+ omxDisp.legends[index] = "#NTS";
+ omxDisp.legendVals[index] = chord->numNotes;
+ }
+ break;
+ case CPARAM_INT_DEGREE:
+ {
+ omxDisp.legends[index] = "DEG";
+ omxDisp.legendVals[index] = chord->degree;
+ }
+ break;
+ case CPARAM_INT_OCTAVE:
+ {
+ omxDisp.legends[index] = "OCT";
+ omxDisp.legendVals[index] = chord->octave;
+ }
+ break;
+ case CPARAM_INT_TRANSPOSE:
+ {
+ omxDisp.legends[index] = "TPS";
+ omxDisp.legendVals[index] = chord->transpose;
+ }
+ break;
+ case CPARAM_INT_SPREAD:
+ {
+ omxDisp.legends[index] = "SPRD";
+ omxDisp.legendVals[index] = chord->spread;
+ }
+ break;
+ case CPARAM_INT_ROTATE:
+ {
+ omxDisp.legends[index] = "ROT";
+ omxDisp.legendVals[index] = chord->rotate;
+ }
+ break;
+ case CPARAM_INT_VOICING:
+ {
+ omxDisp.legends[index] = "VOIC";
+ omxDisp.legendText[index] = kVoicingNames[chord->voicing];
+ }
+ break;
+ case CPARAM_INT_SPRDUPDOWN:
+ {
+ omxDisp.legends[index] = "UPDN";
+ omxDisp.legendText[index] = chord->spreadUpDown ? "ON" : "OFF";
+ }
+ break;
+ case CPARAM_INT_QUARTVOICE:
+ {
+ omxDisp.legends[index] = "QRTV";
+ omxDisp.legendText[index] = chord->quartalVoicing ? "ON" : "OFF";
+ }
+ break;
+ }
+ }
+
+ChordUtil chordUtil;
diff --git a/Archive/OMX-27-firmware/src/utils/chord_util.h b/Archive/OMX-27-firmware/src/utils/chord_util.h
new file mode 100644
index 00000000..0aa9e7b4
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/utils/chord_util.h
@@ -0,0 +1,34 @@
+#pragma once
+#include "../config.h"
+#include "../utils/chord_structs.h"
+#include "../utils/music_scales.h"
+#include "../ClearUI/ClearUI_Input.h"
+// #include "../modes/omx_mode_interface.h"
+// #include "../modes/submodes/submode_clearstorage.h"
+
+// Singleton class for making chords
+class ChordUtil
+{
+public:
+ ChordUtil();
+
+ bool constructChord(ChordSettings *chord, ChordNotes *chordNotes, int8_t autoOctave, int scaleRoot, int scalePattern, bool midiFx);
+ bool constructChordBasic(ChordSettings * chord, ChordNotes * chordNotes, int8_t autoOctave, bool midiFx);
+ bool constructChordInterval(ChordSettings *chord, ChordNotes *chordNotes, int8_t autoOctave, int scaleRoot, int scalePattern, bool midiFx);
+
+ ChordBalanceDetails getChordBalance(uint8_t balance);
+ MusicScales* getMusicScale();
+
+ void onEncoderChangedEditParam(Encoder::Update *enc, ChordSettings *chord, uint8_t selectedParmIndex, uint8_t targetParamIndex, uint8_t paramType);
+ void setupPageLegend(ChordSettings *chord, uint8_t index, uint8_t paramType);
+private:
+ MusicScales musicScale_;
+ ChordBalanceDetails chordBalanceDetails;
+
+ int AddOctave(int note, int8_t octave);
+ int TransposeNote(int note, int8_t semitones);
+
+ void updateChordBalance(uint8_t balance);
+};
+
+extern ChordUtil chordUtil;
diff --git a/Archive/OMX-27-firmware/src/utils/cvNote_util.cpp b/Archive/OMX-27-firmware/src/utils/cvNote_util.cpp
new file mode 100644
index 00000000..0d952c2c
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/utils/cvNote_util.cpp
@@ -0,0 +1,203 @@
+#include "cvNote_util.h"
+#include "../config.h"
+#include "../consts/consts.h"
+
+const char *cvModeDispNames[] = {
+ "LEG",
+ "RTRG"
+};
+
+CVNoteUtil::CVNoteUtil()
+{
+}
+CVNoteUtil::~CVNoteUtil()
+{
+}
+
+const char *CVNoteUtil::getTriggerModeDispName()
+{
+ return cvModeDispNames[triggerMode];
+}
+
+bool CVNoteUtil::isNoteValid(uint8_t midiNoteNum)
+{
+ return midiNoteNum >= cvLowestNote && midiNoteNum < cvHightestNote;
+}
+
+uint8_t CVNoteUtil::midi2CVNote(uint8_t noteNumber)
+{
+ return noteNumber - cvLowestNote;
+}
+uint8_t CVNoteUtil::cv2MidiNote(uint8_t noteNumber)
+{
+ return noteNumber + cvLowestNote;
+}
+
+void CVNoteUtil::cvNoteOn(uint8_t notenum)
+{
+ // Serial.println("cvNoteOn: " + String(notenum));
+
+ if (isNoteValid(notenum) == false)
+ return;
+
+ uint8_t cvNoteNum = midi2CVNote(notenum);
+
+ // Send the latest pitch
+ setPitch(cvNoteNum);
+
+ bool areNotesHeld = cvNotes_.size() > 0;
+
+ if (areNotesHeld)
+ {
+ // See if note is already in queue and remove it
+ auto it = cvNotes_.begin();
+ while (it != cvNotes_.end())
+ {
+ if (it->cvNote == cvNoteNum)
+ {
+ it = cvNotes_.erase(it);
+ }
+ else
+ {
+ ++it;
+ }
+ }
+ }
+
+ // if the queue is too large, remove the oldest note at the front
+ if (cvNotes_.size() >= trackedSize)
+ {
+ // Serial.println("removing front, >= trackedSize");
+ cvNotes_.erase(cvNotes_.begin());
+ }
+
+ CVNoteTracker cvTrackedNote;
+ cvTrackedNote.cvNote = cvNoteNum;
+ cvNotes_.push_back(cvTrackedNote);
+
+ // How should gate be handled?
+ switch (triggerMode)
+ {
+ case CVTRIGMODE_LEGATO:
+ {
+ // Keep gate high while notes are played
+ setGate(true);
+ }
+ break;
+ case CVTRIGMODE_RETRIG:
+ {
+ // if (!pulseGate)
+ // {
+ // pulseGate = true;
+ // setGate(false); // Set gate low for period of time
+ // turnGateOnTime = sysSettings.timeElasped + 10000;
+ // }
+
+ uint32_t currentTime = micros();
+
+ if (areNotesHeld)
+ {
+ if (currentTime > turnGateOnTime)
+ {
+ // Serial.println("pulseGate retrig pulse notes held");
+
+ pulseGate = true;
+ setGate(false); // Set gate low for period of time
+ // turnGateOnTime = sysSettings.timeElasped + 15000;
+ }
+ else
+ {
+ // Serial.println("retrig notes held");
+ setGate(true);
+ }
+ }
+ else
+ {
+ // Serial.println("retrig pulse no notes held");
+ // turn gate on with first note
+ setGate(true);
+ }
+ turnGateOnTime = currentTime + 10000;
+ }
+ break;
+ }
+}
+
+void CVNoteUtil::cvNoteOff(uint8_t notenum)
+{
+ // Serial.println("cvNoteOff: " + String(notenum));
+
+ if (isNoteValid(notenum) == false)
+ return;
+
+ uint8_t cvNoteNum = midi2CVNote(notenum);
+
+ bool areNotesHeld = cvNotes_.size() > 0;
+
+ // This should not happen, note on should have been tracked
+ if (!areNotesHeld)
+ {
+ pulseGate = false;
+ setGate(false);
+ return;
+ }
+
+ // See if note is already in queue and remove it
+ auto it = cvNotes_.begin();
+ while (it != cvNotes_.end())
+ {
+ if (it->cvNote == cvNoteNum)
+ {
+ it = cvNotes_.erase(it);
+ }
+ else
+ {
+ ++it;
+ }
+ }
+
+ areNotesHeld = cvNotes_.size() > 0;
+
+ if(areNotesHeld)
+ {
+ setPitch(cvNotes_[cvNotes_.size() - 1].cvNote);
+ }
+ // No more held notes, turn gate off
+ else
+ {
+ pulseGate = false;
+ setGate(false);
+ }
+}
+
+void CVNoteUtil::loopUpdate(unsigned long elapsedTime)
+{
+ // If notes are held and pulseGate is true, turn gate back on after some time
+ if(pulseGate && micros() > turnGateOnTime)
+ {
+ // Serial.println("pulse gate on loop");
+ setGate(true);
+ pulseGate = false;
+ }
+}
+
+void CVNoteUtil::setGate(bool high)
+{
+ // Serial.println("SetGate: " + String(high));
+
+ // Serial.println("notes Size: " + String(cvNotes_.size()));
+
+ digitalWrite(CVGATE_PIN, high);
+}
+
+void CVNoteUtil::setPitch(uint8_t cvNoteNum)
+{
+ cvPitch = static_cast(roundf(cvNoteNum * stepsPerSemitone)); // map (adjnote, 36, 91, 0, 4080);
+#if T4
+ dac.setVoltage(cvPitch, false);
+#else
+ analogWrite(CVPITCH_PIN, cvPitch);
+#endif
+}
+
+CVNoteUtil cvNoteUtil;
\ No newline at end of file
diff --git a/Archive/OMX-27-firmware/src/utils/cvNote_util.h b/Archive/OMX-27-firmware/src/utils/cvNote_util.h
new file mode 100644
index 00000000..20d883f6
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/utils/cvNote_util.h
@@ -0,0 +1,56 @@
+#pragma once
+
+#include
+#include
+
+enum CVTriggerModes
+{
+ CVTRIGMODE_LEGATO,
+ CVTRIGMODE_RETRIG,
+};
+
+class CVNoteUtil
+{
+public:
+ CVNoteUtil();
+ ~CVNoteUtil();
+
+ uint8_t triggerMode = CVTRIGMODE_LEGATO;
+
+ void loopUpdate(unsigned long elapsedTime);
+
+ void cvNoteOn(uint8_t notenum);
+ void cvNoteOff(uint8_t notenum);
+
+ const char* getTriggerModeDispName();
+
+ int cvPitch;
+
+// static const uint8_t midiMiddleC = 60;
+// static const uint8_t midiLowestNote = midiMiddleC - 3 * 12; // 3 is how many octaves under middle c
+// static const int midiHightestNote = midiLowestNote + int(fullRangeV * 12) - 1;
+
+private:
+ struct CVNoteTracker
+ {
+ uint8_t cvNote : 6; // 0 - 54, note gets 24 added to it
+ };
+
+ bool pulseGate = false;
+
+ uint32_t turnGateOnTime;
+
+ static const uint8_t trackedSize = 16;
+
+ std::vector cvNotes_; // Keeps track of which notes are being played
+
+ static bool isNoteValid(uint8_t midiNoteNum);
+
+ void setGate(bool high);
+ void setPitch(uint8_t cvNoteNum);
+
+ static uint8_t midi2CVNote(uint8_t noteNumber);
+ static uint8_t cv2MidiNote(uint8_t noteNumber);
+};
+
+extern CVNoteUtil cvNoteUtil;
\ No newline at end of file
diff --git a/Archive/OMX-27-firmware/src/utils/logic_util.h b/Archive/OMX-27-firmware/src/utils/logic_util.h
new file mode 100644
index 00000000..edbcf547
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/utils/logic_util.h
@@ -0,0 +1,8 @@
+#pragma once
+
+#define WRAP(a, b) ((b) + ((a) % (b))) % (b)
+#define ARRAYLEN(x) (sizeof(x) / sizeof(x[0]))
+#define SGN(x) ((x) < 0 ? -1 : 1)
+#define MIN(a, b) ((a) < (b) ? (a) : (b))
+#define MAX(a, b) ((a) > (b) ? (a) : (b))
+#define CLAMP(a, min, max) (MAX(MIN(a, max), min))
diff --git a/Archive/OMX-27-firmware/src/utils/music_scales.cpp b/Archive/OMX-27-firmware/src/utils/music_scales.cpp
new file mode 100644
index 00000000..f7306fc8
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/utils/music_scales.cpp
@@ -0,0 +1,601 @@
+#include
+#include "music_scales.h"
+#include "../consts/colors.h"
+#include "logic_util.h"
+// #include "config.h"
+// #include "omx_leds.h"
+#include
+
+const uint8_t rainbowSaturation = 127;
+const uint8_t scaleBrightness = 200;
+
+const auto ROOTNOTECOLOR = 0xA2A2FF;
+const auto INSCALECOLOR = 0x000090;
+
+String tempFullNoteName;
+
+#include
+extern Adafruit_NeoPixel strip;
+
+const int8_t scalePatterns[][7] = {
+ // major / ionian
+ {0, 2, 4, 5, 7, 9, 11},
+ // dorian
+ {0, 2, 3, 5, 7, 9, 10},
+ // phrygian
+ {0, 1, 3, 5, 7, 8, 10},
+ // lydian
+ {0, 2, 4, 6, 7, 9, 11},
+ // mixolydian
+ {0, 2, 4, 5, 7, 9, 10},
+ // minor / aeolian
+ {0, 2, 3, 5, 7, 8, 10},
+ // locrian
+ {0, 1, 3, 5, 6, 8, 10},
+
+ // melodic minor
+ {0, 2, 3, 5, 7, 9, 11},
+ // dorian b2
+ {0, 1, 3, 5, 7, 9, 10},
+ // lydian #5
+ {0, 2, 4, 6, 8, 9, 11},
+ // lydian b7
+ {0, 2, 4, 6, 7, 9, 10},
+ // mixolydian b6
+ {0, 2, 4, 5, 7, 8, 10},
+ // half-diminished (locrian natural 2)
+ {0, 2, 3, 5, 6, 8, 10},
+ // altered (super locrian)
+ {0, 1, 3, 4, 6, 8, 10},
+
+ // harmonic minor
+ {0, 2, 3, 5, 7, 8, 11},
+ // locrian 6
+ {0, 1, 3, 5, 6, 9, 10},
+ // ionian #5
+ {0, 2, 4, 5, 8, 9, 11},
+ // dorian #4
+ {0, 2, 3, 6, 7, 9, 10},
+ // phrygian dominant
+ {0, 1, 4, 5, 7, 8, 10},
+ // lydian #2
+ {0, 3, 4, 6, 7, 9, 11},
+ // super locrian bb7
+ {0, 1, 3, 4, 6, 8, 9},
+
+ // double harmonic
+ {0, 1, 4, 5, 7, 8, 11},
+ // lydian #2#6
+ {0, 3, 4, 6, 7, 10, 11},
+ // ultraphrygian
+ {0, 1, 3, 4, 7, 8, 9},
+ // hungarian
+ {0, 2, 3, 6, 7, 8, 11},
+ // oriental
+ {0, 1, 4, 5, 6, 9, 10},
+ // ionian #2#5
+ {0, 3, 4, 5, 8, 9, 11},
+ // locrian bb3bb7
+ {0, 2, 3, 5, 6, 8, 9},
+
+ // pentatonic scales
+ // major blues
+ {0, 2, 3, 4, 7, 9, -1},
+ // minor blues
+ {0, 3, 5, 6, 7, 10, -1},
+ // major pentatonic
+ {0, 2, 4, 7, 9, -1, -1},
+ // minor pentatonic
+ {0, 3, 5, 7, 10, -1, -1},
+ // in sen (japanese)
+ {0, 1, 5, 7, 10, -1, -1},
+ // iwato
+ {0, 1, 5, 6, 10, -1, -1},
+ // yo
+ {0, 2, 5, 7, 9, -1, -1},
+ // hirajoshi
+ {0, 2, 3, 7, 8, -1, -1},
+ // egyptian
+ {0, 2, 5, 7, 10, -1, -1},
+};
+
+const char *scaleNames[] = {
+ "major",
+ "dorian",
+ "phrygian",
+ "lydian",
+ "mixolydian",
+ "minor",
+ "locrian",
+
+ "mel minor",
+ "dorian b2",
+ "lydian #5",
+ "lydian b7",
+ "mixo b6",
+ "half-dim",
+ "altered",
+
+ "harm minor",
+ "locrian 6",
+ "ionian #5",
+ "dorian #4",
+ "phrygian dom",
+ "lydian #2",
+ "sup loc bb7",
+
+ "dbl harm.maj",
+ "lydian #2#6",
+ "ultraphrygian",
+ "hungarian",
+ "oriental",
+ "ionian #2#5",
+ "loc bb3bb7",
+
+ "blues maj",
+ "blues min",
+ "penta maj",
+ "penta min",
+ "in sen",
+ "iwato",
+ "yo",
+ "hirajoshi",
+ "egyptian",
+};
+
+const char *noteNames[] = {
+ "C ",
+ "C#",
+ "D ",
+ "D#",
+ "E ",
+ "F ",
+ "F#",
+ "G ",
+ "G#",
+ "A ",
+ "A#",
+ "B ",
+};
+
+const char *noteNamesNoFormat[] = {
+ "C",
+ "C#",
+ "D",
+ "D#",
+ "E",
+ "F",
+ "F#",
+ "G",
+ "G#",
+ "A",
+ "A#",
+ "B",
+};
+
+void MusicScales::calculateScaleIfModified(uint8_t scaleRoot, uint8_t scalePattern)
+{
+ if (scaleRoot == rootNote && scalePattern == scaleIndex)
+ return;
+
+ calculateScale(scaleRoot, scalePattern);
+}
+
+void MusicScales::calculateScale(uint8_t scaleRoot, uint8_t scalePattern)
+{
+ if (scaleRoot != rootNote && scalePattern != scaleIndex)
+ {
+ scaleRemapCalculated_ = false;
+ }
+
+ rootNote = scaleRoot;
+ scaleIndex = scalePattern;
+ auto pattern = getScalePattern(scalePattern);
+
+ // auto sPattern2 = scalePatterns[scalePattern];
+
+ if (scalePattern == -1)
+ {
+ // disabled
+ for (int n = 0; n < 12; n++)
+ {
+ scaleOffsets[n] = -1;
+ scaleDegrees[n] = -1;
+ scaleColors[n] = LEDOFF;
+ }
+ }
+ else
+ {
+ for (int n = 0; n < 12; n++)
+ {
+ int offset = -1;
+ int degree = -1;
+
+ for (int j = 0; j < 7; j++)
+ {
+ // int v = scalePatterns[scalePattern][j];
+ int v = pattern[j];
+
+ if (v == -1)
+ {
+ continue;
+ }
+ if ((scaleRoot + v) % 12 == n)
+ {
+ offset = v;
+ degree = j;
+ break;
+ }
+ }
+ scaleOffsets[n] = offset;
+ scaleDegrees[n] = degree;
+ if (degree == -1)
+ {
+ scaleColors[n] = LEDOFF;
+ }
+ else
+ {
+ if (degree == 0)
+ {
+ scaleColors[n] = ROOTNOTECOLOR;
+ }
+ else
+ {
+ scaleColors[n] = INSCALECOLOR;
+ }
+
+ // Use for rainbow scale
+ // scaleColors[n] = strip.gamma32(strip.ColorHSV((65535 / 12) * offset, rainbowSaturation, scaleBrightness));
+ }
+ }
+
+ int k = 0;
+ int octave = 0;
+
+ // Populate offsets for group16 mode
+ for (int i = 0; i < 16; i++)
+ {
+ int offset = pattern[k];
+
+ if (offset == -1)
+ {
+ k = 0;
+ offset = pattern[k];
+ octave++;
+ }
+ k++;
+
+ group16Offsets[i] = offset + 12 * octave;
+
+ if (k >= 7)
+ {
+ k = 0;
+ octave++;
+ }
+ }
+ }
+ scaleLength = 0;
+ for (int j = 0; j < 7; j++)
+ {
+ int v = pattern[j];
+ if (v != -1)
+ {
+ scaleLength++;
+ }
+ }
+
+ scaleCalculated = true;
+}
+
+uint8_t MusicScales::getNumScales()
+{
+ return ARRAYLEN(scalePatterns);
+}
+
+bool MusicScales::isNoteInScale(int8_t noteNum)
+{
+ // Serial.println((String)"isNoteInScale: " + noteNum );
+ if (!scaleCalculated || noteNum < 0 || noteNum > 127)
+ {
+ return false;
+ }
+
+ int noteIndex = noteNum % 12;
+ bool inScale = scaleColors[noteIndex] != LEDOFF;
+
+ // Serial.println((String)"noteIndex: " + noteNum + " inScale: " + (inScale ? "true" : "false"));
+
+ return inScale;
+}
+
+// This takes a incoming note and forces it into the current scale
+int8_t MusicScales::remapNoteToScale(uint8_t noteNumber)
+{
+ if (!scaleCalculated)
+ {
+ calculateScale(rootNote, scaleIndex);
+ }
+
+ if (noteNumber > 127)
+ {
+ return -1;
+ }
+
+ if (!scaleRemapCalculated_)
+ {
+ calculateRemap();
+ }
+
+ int8_t noteIndex = noteNumber % 12;
+ int8_t octave = noteNumber / 12;
+
+ int8_t remapedNoteIndex = scaleRemapper[noteIndex];
+
+ if (remapedNoteIndex > noteIndex)
+ {
+ octave--;
+ }
+
+ int newNoteNumber = octave * 12 + remapedNoteIndex;
+
+ // note out of range, kill
+ if (newNoteNumber < 0 || newNoteNumber > 127)
+ {
+ return -1;
+ }
+
+ return newNoteNumber;
+}
+
+void MusicScales::calculateRemap()
+{
+ if (scaleIndex < 0)
+ {
+ for (uint8_t i = 0; i < 12; i++)
+ {
+ scaleRemapper[i] = i; // Chromatic scale
+ }
+
+ scaleRemapCalculated_ = true;
+ return;
+ }
+
+ auto scalePattern = getScalePattern(scaleIndex);
+
+ uint8_t sIndex = 0;
+ uint8_t lastNoteIndex = 0;
+
+ // looks through 12 notes, and sets each note to last note in scale
+ // so notes out of scale get rounded down to the previous note in the scale.
+ for (uint8_t i = 0; i < 12; i++)
+ {
+ if (sIndex < 7 && scalePattern[sIndex] == i)
+ {
+ lastNoteIndex = i;
+ sIndex++;
+ }
+ scaleRemapper[i] = (lastNoteIndex + rootNote) % 12;
+ }
+
+ if (rootNote > 0)
+ {
+ // rotate the scale to root
+ int8_t temp[12];
+
+ uint8_t val = 12 - rootNote;
+
+ for (uint8_t i = 0; i < 12; i++)
+ {
+ temp[i] = scaleRemapper[(i + val) % 12];
+ }
+ for (int i = 0; i < 12; i++)
+ {
+ scaleRemapper[i] = temp[i];
+ }
+ }
+
+ scaleRemapCalculated_ = true;
+}
+
+int MusicScales::getGroup16Note(uint8_t keyNum, int8_t octave)
+{
+ // 1,2, 3,4,5, 6,7, 8,9,10,
+ // 11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26
+
+ if (keyNum < 11 || keyNum > 26 || scaleIndex < 0)
+ return -1;
+
+ int stepIndex = keyNum - 12; // C will be root note
+
+ int adjnote;
+
+ if (keyNum == 11) // edge case to make line up with C note
+ {
+ int offset = -1;
+
+ for (int j = 0; j < 7; j++) // find last valid offset of scale
+ {
+ int o = scalePatterns[scaleIndex][j];
+ if (o != -1)
+ {
+ offset = o;
+ }
+ }
+
+ if (offset == -1)
+ return -1;
+
+ int firstNote = group16Offsets[0] + rootNote + 60 + (octave * 12);
+ adjnote = firstNote + offset - 12; // lower by 1 octave
+ }
+ else
+ {
+ adjnote = group16Offsets[stepIndex] + rootNote + 60 + (octave * 12);
+ }
+
+ // adjnote = constrain(adjnote, -1, 127);
+
+ return adjnote;
+}
+
+int8_t MusicScales::getNoteByDegree(uint8_t degree, int8_t octave)
+{
+ // degree should be less than 16
+ if (degree >= 16)
+ return -1;
+
+ int adjnote;
+
+ if (scaleIndex < 0)
+ {
+ // Chromatically offset
+ adjnote = 60 + rootNote + degree + (octave * 12);
+ // Serial.println("Chromatic note: " + String(adjnote));
+ }
+ else
+ {
+ adjnote = group16Offsets[degree] + rootNote + 60 + (octave * 12);
+ }
+ if (adjnote > 127 || adjnote < -1)
+ adjnote = -1;
+ adjnote = constrain(adjnote, -1, 127);
+
+ return (int8_t)adjnote;
+}
+
+uint8_t MusicScales::getDegreeFromNote(uint8_t noteNumber, int8_t rootNote, int8_t scalePatIndex)
+{
+ uint8_t noteFlat = (noteNumber + 12 - rootNote) % 12;
+
+ // Chromatic
+ if(scalePatIndex < 0)
+ {
+ return noteFlat;
+ }
+
+ // Root = 62 - D
+ // Pat = Maj : D E F# G A B C#
+ // note 64 = E
+ // noteFlat should = 2
+ // degree should = 2
+ // note 65 = F
+ // noteFlag should = 3
+ // degree should = 2, round down
+
+ // Maj scale pattern {0, 2, 4, 5, 7, 9, 11},
+
+ // Scale pattern should be sorted
+ auto scalePat = getScalePattern(scalePatIndex);
+
+ for(uint8_t i = 0; i < 7; i++)
+ {
+ int8_t off0 = scalePat[i];
+ int8_t off1 = -1;
+
+ if(i < 6)
+ {
+ off1 = scalePat[i + 1];
+ }
+
+ // boom, perfectly in scale
+ if(noteFlat == off0)
+ {
+ return i;
+ }
+ // reached the end of the scale
+ if(off1 < 0)
+ {
+ return i;
+ }
+ // next scale offset matches this note
+ if(noteFlat == off1)
+ {
+ return i + 1;
+ }
+ // ot oh, note is not in the scale
+ if(noteFlat > off0 && noteFlat < off1)
+ {
+ // Compare distance to offsets
+ // If same distance, round down
+ // If shorter distance to off1, choose off1
+ if(off1 - noteFlat < noteFlat - off0)
+ {
+ return i + 1;
+ }
+ else
+ {
+ return i;
+ }
+ }
+ }
+
+ // Note sure how we'd get here
+ return noteFlat;
+}
+
+int MusicScales::getScaleColor(uint8_t noteIndex)
+{
+ if (!scaleCalculated)
+ return LEDOFF;
+ return scaleColors[noteIndex];
+}
+
+int MusicScales::getGroup16Color(uint8_t keyNum)
+{
+ if (!scaleCalculated || keyNum < 11 || keyNum > 26 || scaleIndex < 0)
+ return LEDOFF;
+
+ int note = getGroup16Note(keyNum, 4);
+
+ if (note < 0)
+ return LEDOFF;
+
+ note = note % 12;
+
+ return scaleColors[note];
+}
+
+const char *MusicScales::getNoteName(uint8_t noteIndex, bool removeSpaces)
+{
+ // noteIndex = constrain(noteIndex, 0, 11);
+ if (removeSpaces)
+ {
+ return noteNamesNoFormat[noteIndex % 12];
+ }
+ return noteNames[noteIndex % 12];
+}
+
+const char *MusicScales::getFullNoteName(uint8_t noteNumber)
+{
+ int8_t octave = (noteNumber / 12) - 2;
+ tempFullNoteName = String(noteNamesNoFormat[noteNumber % 12] + String(octave));
+
+ // strcpy(fullNoteNameBuf, noteNamesNoFormat[noteNumber % 12]);
+ // strcat(fullNoteNameBuf, itoa(octave,fullNoteNameBuf,10));
+
+ // return fullNoteNameBuf;
+ // int8_t octave = (noteNumber / 12) - 2;
+
+ // String newString = noteNamesNoFormat[noteNumber % 12] + String(octave);
+
+ // tempFullNoteName = newString;
+
+ return tempFullNoteName.c_str();
+}
+
+const char *MusicScales::getScaleName(uint8_t scaleIndex)
+{
+ if (scaleIndex < 0 || scaleIndex >= getNumScales())
+ return "off";
+ return scaleNames[scaleIndex];
+}
+
+int MusicScales::getScaleLength()
+{
+ return scaleLength;
+}
+
+const int8_t *MusicScales::getScalePattern(uint8_t patIndex)
+{
+ return scalePatterns[patIndex];
+}
diff --git a/Archive/OMX-27-firmware/src/utils/music_scales.h b/Archive/OMX-27-firmware/src/utils/music_scales.h
new file mode 100644
index 00000000..030c43c6
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/utils/music_scales.h
@@ -0,0 +1,59 @@
+#pragma once
+
+class MusicScales
+{
+public:
+ // int scaleDegrees[12];
+ // int scaleOffsets[12];
+ // int scaleColors[12];
+ // const char* scaleNames[];
+ // const char* noteNames[];
+ // const int scalePatterns[][7];
+
+ void calculateScaleIfModified(uint8_t scaleRoot, uint8_t scalePattern);
+ void calculateScale(uint8_t scaleRoot, uint8_t scalePattern);
+ static uint8_t getNumScales();
+ // int scaleLength;
+
+ // returns true if note 0-11 is in the currently calculated scale. NoteNum should be a midi note
+ bool isNoteInScale(int8_t noteNum);
+
+ // This takes a incoming note and forces it into the current scale
+ int8_t remapNoteToScale(uint8_t noteNumber);
+
+ // returns a note in the scale if key is one of the lower 16. Returns -1 otherwise.
+ // TODO This won't work unless returns int, won't work with int8_t not sure why
+ int getGroup16Note(uint8_t keyNum, int8_t octave);
+
+ int8_t getNoteByDegree(uint8_t degree, int8_t octave);
+ static uint8_t getDegreeFromNote(uint8_t noteNumber, int8_t rootNote, int8_t scalePatIndex);
+
+ // Returns a color for the note
+ int getScaleColor(uint8_t noteIndex);
+
+ int getGroup16Color(uint8_t keyNum);
+
+ static const char *getNoteName(uint8_t noteIndex, bool removeSpaces = false);
+ static const char *getFullNoteName(uint8_t noteNumber);
+ static const char *getScaleName(uint8_t scaleIndex);
+ static const int8_t *getScalePattern(uint8_t patIndex);
+ int getScaleLength();
+
+private:
+ bool scaleCalculated = false;
+ bool scaleRemapCalculated_ = false;
+
+ int8_t scaleOffsets[12];
+ int8_t scaleDegrees[12];
+ int scaleColors[12];
+ uint8_t scaleLength = 0;
+
+ int8_t scaleRemapper[12];
+
+ int8_t rootNote;
+ int8_t scaleIndex;
+
+ void calculateRemap();
+
+ int group16Offsets[16]; // 16 offsets for group16 mode
+};
diff --git a/Archive/OMX-27-firmware/src/utils/omx_util.cpp b/Archive/OMX-27-firmware/src/utils/omx_util.cpp
new file mode 100644
index 00000000..82e22373
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/utils/omx_util.cpp
@@ -0,0 +1,678 @@
+#include
+
+#include "omx_util.h"
+#include "../consts/consts.h"
+#include "../midi/midi.h"
+#include "../consts/colors.h"
+#include "../hardware/omx_leds.h"
+#include "../hardware/omx_disp.h"
+#include "../midi/noteoffs.h"
+#include "../modes/sequencer.h"
+#include "cvNote_util.h"
+
+void OmxUtil::setup()
+{
+}
+
+void OmxUtil::sendPots(int val, int channel)
+{
+ MM::sendControlChange(pots[potSettings.potbank][val], potSettings.analogValues[val], channel);
+ potSettings.potCC = pots[potSettings.potbank][val];
+ potSettings.potVal = potSettings.analogValues[val];
+ potSettings.potValues[val] = potSettings.potVal;
+}
+
+float OmxUtil::randFloat()
+{
+ return static_cast (rand()) / static_cast (RAND_MAX);
+}
+
+float OmxUtil::lerp(float a, float b, float t)
+{
+ return a + t * (b - a);
+}
+
+void OmxUtil::advanceClock(OmxModeInterface *activeOmxMode, Micros advance)
+{
+ // advance is delta in Micros from previous loop update to this loop update.
+
+ // XXXXXXXXXXXXXXXXXXXXXXXX
+ // Txxxxxxxxxxxxxxxxxxxxxxx - Quarter Note - 24 ticks
+ // TxxxxxxxxxxxTxxxxxxxxxxx - 8th note - 12 ticks
+ // TxxxxxTxxxxxTxxxxxTxxxxx - 16th note - 6 ticks
+ // TxxTxxTxxTxxTxxTxxTxxTxx - 32nd note - 3 ticks
+
+ // TxxxxxT
+ // xTxxxxT
+ // xxTxxxT
+
+
+ activeOmxMode_ = activeOmxMode;
+
+ signed long long adv = advance;
+
+ // Not sure what advantage of doing the time comparison
+ // in a while loop like this is
+ // Maybe so if there is a long advance multiple clocks
+ // will get fired to catch up?
+ // Keeping like this for now as it works.
+ while (adv >= timeToNextClock)
+ {
+ adv -= timeToNextClock;
+
+ // if (sendClocks_)
+ // {
+ // MM::sendClock();
+ // }
+
+ seqConfig.currentFrameMicros = micros();
+ seqConfig.lastClockMicros = micros();
+
+ // if(seqConfig.currentClockTick == 0)
+ // {
+ // Serial.println("Quarter Note");
+ // }
+
+ // Midi Clock should be sent out at ppq of 24
+ // Since ppq is 96, every 4 clock ticks should send midi clock
+ if(seqConfig.midiOutClockTick % 4 == 0)
+ {
+ // Should always send clock
+ // This way external gear can update themselves
+ if (clockConfig.send_always)
+ {
+ MM::sendClock();
+ }
+ }
+
+ if (activeOmxMode_ != nullptr)
+ {
+ // Update internally at PPQ of 94
+ activeOmxMode_->onClockTick();
+ }
+
+ // timeToNextClock = clockConfig.ppqInterval * (PPQ / 24); // ppqInt=5.208ms * 4 = 20.83 milliseconds for 120 bpm, 120 bpm = 2 beats per second, a beat being a quarter note
+
+ seqConfig.midiOutClockTick = (seqConfig.midiOutClockTick + 1) % PPQ;
+ seqConfig.currentClockTick = (seqConfig.currentClockTick + 1) % (PPQ * 4);
+
+ timeToNextClock = clockConfig.ppqInterval;
+ }
+ timeToNextClock = timeToNextClock - adv;
+}
+
+void OmxUtil::resetPPQCounter()
+{
+ seqConfig.currentClockTick = 0;
+}
+
+void OmxUtil::advanceSteps(Micros advance)
+{
+ static Micros timeToNextStep = 0;
+ // static Micros stepnow = micros();
+ while (advance >= timeToNextStep)
+ {
+ advance -= timeToNextStep;
+ timeToNextStep = clockConfig.ppqInterval;
+ auto currentMicros = micros();
+
+ pendingNoteHistory.clearIfChanged(currentMicros);
+
+ // turn off any expiring notes
+ pendingNoteOffs.play(currentMicros);
+
+ // turn on any pending notes
+ pendingNoteOns.play(currentMicros);
+ }
+ timeToNextStep -= advance;
+}
+
+void OmxUtil::setGlobalSwing(int swng_amt)
+{
+ for (int z = 0; z < NUM_SEQ_PATTERNS; z++)
+ {
+ sequencer.getPattern(z)->swing = swng_amt;
+ }
+}
+
+void OmxUtil::resetClocks()
+{
+ // BPM tempo to step_delay calculation
+ // 60000000 = 60 secs
+ clockConfig.ppqInterval = 60000000 / (PPQ * clockConfig.clockbpm); // ppq interval is in microseconds, 96 * 120 = 11520, 60000000 / 11520 = 52083 microsecond, * 0.001 = 5.208 milliseconds,
+ clockConfig.step_micros = clockConfig.ppqInterval * (PPQ / 4); // 16th note step in microseconds (quarter of quarter note)
+
+ // 16th note step length in milliseconds
+ clockConfig.step_delay = clockConfig.step_micros * 0.001; // ppqInterval * 0.006; // 60000 / clockbpm / 4;
+}
+
+void OmxUtil::restartClocks()
+{
+ resetClocks();
+ timeToNextClock = 0;
+ seqConfig.currentFrameMicros = micros();
+ seqConfig.lastClockMicros = seqConfig.currentFrameMicros;
+}
+
+void OmxUtil::startClocks()
+{
+ sendClocks_ = true;
+ clockConfig.send_always = true;
+ MM::startClock();
+}
+
+void OmxUtil::resumeClocks()
+{
+ sendClocks_ = true;
+ clockConfig.send_always = true;
+ MM::continueClock();
+}
+
+void OmxUtil::stopClocks()
+{
+ sendClocks_ = false;
+ clockConfig.send_always = false;
+ MM::stopClock();
+}
+
+bool OmxUtil::areClocksRunning()
+{
+ return sendClocks_;
+}
+
+// void OmxUtil::cvNoteOn(uint8_t notenum)
+// {
+// if (notenum >= cvLowestNote && notenum < cvHightestNote)
+// {
+// midiSettings.pitchCV = static_cast(roundf((notenum - cvLowestNote) * stepsPerSemitone)); // map (adjnote, 36, 91, 0, 4080);
+// digitalWrite(CVGATE_PIN, HIGH);
+// // analogWrite(CVPITCH_PIN, midiSettings.pitchCV);
+// #if T4
+// dac.setVoltage(midiSettings.pitchCV, false);
+// #else
+// analogWrite(CVPITCH_PIN, midiSettings.pitchCV);
+// #endif
+// }
+// }
+// void OmxUtil::cvNoteOff(uint8_t notenum)
+// {
+// if (notenum >= cvLowestNote && notenum < cvHightestNote)
+// {
+// digitalWrite(CVGATE_PIN, LOW);
+// // analogWrite(CVPITCH_PIN, 0);
+// }
+// }
+
+void OmxUtil::midiNoteOn(int notenum, int velocity, int channel)
+{
+ midiNoteOn(nullptr, notenum, velocity, channel);
+}
+
+// #### Outbound MIDI Mode note on/off
+void OmxUtil::midiNoteOn(MusicScales *scale, int notenum, int velocity, int channel)
+{
+ int adjnote = notes[notenum] + (midiSettings.octave * 12); // adjust key for octave range
+
+ if (scale != nullptr)
+ {
+ if (scaleConfig.group16)
+ {
+ adjnote = scale->getGroup16Note(notenum, midiSettings.octave);
+ }
+ else
+ {
+ if (scaleConfig.lockScale && scale->isNoteInScale(adjnote) == false)
+ return; // Only play note if in scale
+ }
+ }
+
+ midiSettings.rrChannel = (midiSettings.rrChannel % midiSettings.midiRRChannelCount) + 1;
+ int adjchan = midiSettings.rrChannel;
+
+ if (adjnote >= 0 && adjnote < 128)
+ {
+ midiSettings.midiLastNote = adjnote;
+ midiSettings.midiLastVel = velocity;
+
+ // keep track of adjusted note when pressed so that when key is released we send
+ // the correct note off message
+ midiSettings.midiKeyState[notenum] = adjnote;
+
+ // RoundRobin Setting?
+ if (midiSettings.midiRoundRobin)
+ {
+ adjchan = midiSettings.rrChannel + midiSettings.midiRRChannelOffset;
+ }
+ else
+ {
+ adjchan = channel;
+ }
+ midiSettings.midiChannelState[notenum] = adjchan;
+ MM::sendNoteOn(adjnote, velocity, adjchan);
+ // CV
+ cvNoteUtil.cvNoteOn(adjnote);
+ }
+ else
+ {
+ return; // no note sent, don't light LEDs
+ }
+
+ strip.setPixelColor(notenum, MIDINOTEON); // Set pixel's color (in RAM)
+ omxLeds.setDirty();
+ omxDisp.setDirty();
+}
+
+void OmxUtil::allOff()
+{
+ for (uint8_t i = 0; i < 27; i++)
+ {
+ if (midiSettings.midiKeyState[i] >= 0)
+ {
+ midiNoteOff(i, midiSettings.midiChannelState[i]);
+ }
+ }
+}
+
+void OmxUtil::midiNoteOff(int notenum, int channel)
+{
+ // we use the key state captured at the time we pressed the key to send the correct note off message
+ int adjnote = midiSettings.midiKeyState[notenum];
+ int adjchan = midiSettings.midiChannelState[notenum];
+ if (adjnote >= 0 && adjnote < 128)
+ {
+ MM::sendNoteOff(adjnote, 0, adjchan);
+ // CV off
+ cvNoteUtil.cvNoteOff(adjnote);
+ midiSettings.midiKeyState[notenum] = -1;
+ }
+
+ strip.setPixelColor(notenum, LEDOFF);
+ omxLeds.setDirty();
+ omxDisp.setDirty();
+}
+
+MidiNoteGroup OmxUtil::midiNoteOn2(MusicScales *scale, int notenum, int velocity, int channel)
+{
+ int adjnote = notes[notenum] + (midiSettings.octave * 12); // adjust key for octave range
+
+ MidiNoteGroup noteGroup;
+
+ if (scale != nullptr)
+ {
+ if (scaleConfig.group16)
+ {
+ adjnote = scale->getGroup16Note(notenum, midiSettings.octave);
+ }
+ else
+ {
+ if (scaleConfig.lockScale && scale->isNoteInScale(adjnote) == false)
+ {
+ noteGroup.noteNumber = 255;
+ return noteGroup; // Only play note if in scale
+ }
+ }
+ }
+
+ midiSettings.rrChannel = (midiSettings.rrChannel % midiSettings.midiRRChannelCount) + 1;
+ int adjchan = midiSettings.rrChannel;
+
+ if (adjnote >= 0 && adjnote < 128)
+ {
+ midiSettings.midiLastNote = adjnote;
+ midiSettings.midiLastVel = velocity;
+
+ // keep track of adjusted note when pressed so that when key is released we send
+ // the correct note off message
+ midiSettings.midiKeyState[notenum] = adjnote;
+
+ // RoundRobin Setting?
+ if (midiSettings.midiRoundRobin)
+ {
+ adjchan = midiSettings.rrChannel + midiSettings.midiRRChannelOffset;
+ }
+ else
+ {
+ adjchan = channel;
+ }
+ midiSettings.midiChannelState[notenum] = adjchan;
+
+ noteGroup.noteNumber = adjnote;
+ noteGroup.velocity = velocity;
+ noteGroup.channel = adjchan;
+ noteGroup.stepLength = 0;
+ noteGroup.sendMidi = true;
+ noteGroup.sendCV = true;
+ noteGroup.noteonMicros = micros();
+
+ // MM::sendNoteOn(adjnote, velocity, adjchan);
+ // // CV
+ // cvNoteOn(adjnote);
+ }
+ else
+ {
+ noteGroup.noteNumber = 255;
+ return noteGroup; // no note sent, don't light LEDs
+ }
+
+ strip.setPixelColor(notenum, MIDINOTEON); // Set pixel's color (in RAM)
+ omxLeds.setDirty();
+ omxDisp.setDirty();
+
+ return noteGroup;
+}
+
+MidiNoteGroup OmxUtil::midiNoteOff2(int notenum, int channel)
+{
+ // we use the key state captured at the time we pressed the key to send the correct note off message
+ int adjnote = midiSettings.midiKeyState[notenum];
+ int adjchan = midiSettings.midiChannelState[notenum];
+
+ MidiNoteGroup noteGroup;
+ noteGroup.noteOff = true;
+
+ if (adjnote >= 0 && adjnote < 128)
+ {
+ // MM::sendNoteOff(adjnote, 0, adjchan);
+ // CV off
+ // cvNoteOff();
+ midiSettings.midiKeyState[notenum] = -1;
+
+ noteGroup.noteNumber = adjnote;
+ noteGroup.velocity = 0;
+ noteGroup.channel = adjchan;
+ noteGroup.stepLength = 0;
+ noteGroup.sendMidi = true;
+ noteGroup.sendCV = true;
+ noteGroup.noteonMicros = micros();
+ }
+ else
+ {
+ noteGroup.noteNumber = 255;
+ return noteGroup;
+ }
+
+ strip.setPixelColor(notenum, LEDOFF);
+ omxLeds.setDirty();
+ omxDisp.setDirty();
+
+ return noteGroup;
+}
+
+MidiNoteGroup OmxUtil::midiDrumNoteOn(uint8_t keyIndex, uint8_t notenum, int velocity, int channel)
+{
+ MidiNoteGroup noteGroup;
+
+ // Not a valid note
+ if(notenum >= 128)
+ {
+ noteGroup.noteNumber = 255;
+ return noteGroup;
+ }
+
+ // keep track of adjusted note when pressed so that when key is released we send
+ // the correct note off message
+ midiSettings.midiKeyState[keyIndex] = notenum;
+ midiSettings.midiChannelState[keyIndex] = channel;
+
+ noteGroup.noteNumber = notenum;
+ noteGroup.velocity = velocity;
+ noteGroup.channel = channel;
+ noteGroup.stepLength = 0;
+ noteGroup.sendMidi = true;
+ noteGroup.sendCV = true;
+ noteGroup.noteonMicros = micros();
+
+ midiSettings.midiLastNote = notenum;
+ midiSettings.midiLastVel = velocity;
+ omxLeds.setDirty();
+ omxDisp.setDirty();
+
+ return noteGroup;
+}
+
+MidiNoteGroup OmxUtil::midiDrumNoteOff(uint8_t keyIndex)
+{
+ // we use the key state captured at the time we pressed the key to send the correct note off message
+ int adjnote = midiSettings.midiKeyState[keyIndex];
+ int adjchan = midiSettings.midiChannelState[keyIndex];
+
+ MidiNoteGroup noteGroup;
+ noteGroup.noteOff = true;
+
+ if (adjnote >= 0 && adjnote < 128)
+ {
+ midiSettings.midiKeyState[keyIndex] = -1;
+
+ noteGroup.noteNumber = adjnote;
+ noteGroup.velocity = 0;
+ noteGroup.channel = adjchan;
+ noteGroup.stepLength = 0;
+ noteGroup.sendMidi = true;
+ noteGroup.sendCV = true;
+ noteGroup.noteonMicros = micros();
+ }
+ else
+ {
+ noteGroup.noteNumber = 255;
+ return noteGroup;
+ }
+
+ omxLeds.setDirty();
+ omxDisp.setDirty();
+
+ return noteGroup;
+}
+
+void OmxUtil::onEncoderChangedEditParam(Encoder::Update *enc, uint8_t selectedParmIndex, uint8_t targetParamIndex, uint8_t paramType)
+{
+ onEncoderChangedEditParam(enc, nullptr, selectedParmIndex, targetParamIndex, paramType);
+}
+
+void OmxUtil::onEncoderChangedEditParam(Encoder::Update *enc, MusicScales *musicScale, uint8_t selectedParmIndex, uint8_t targetParamIndex, uint8_t paramType)
+{
+ if (selectedParmIndex != targetParamIndex)
+ return;
+
+ auto amtSlow = enc->accel(1);
+ auto amtFast = enc->accel(5);
+
+ switch (paramType)
+ {
+ case GPARAM_MOUT_OCT:
+ {
+ midiSettings.octave = constrain(midiSettings.octave + amtSlow, -5, 4);
+ }
+ break;
+ case GPARAM_MOUT_CHAN:
+ {
+ sysSettings.midiChannel = constrain(sysSettings.midiChannel + amtSlow, 1, 16);
+ }
+ break;
+ case GPARAM_MOUT_VEL:
+ {
+ midiSettings.defaultVelocity = constrain((int)midiSettings.defaultVelocity + amtFast, 0, 127); // cast to int to prevent rollover
+ }
+ break;
+ case GPARAM_MIDI_THRU:
+ {
+ midiSettings.midiSoftThru = constrain(midiSettings.midiSoftThru + amtSlow, 0, 1);
+ }
+ break;
+ case GPARAM_POTS_PBANK:
+ {
+ potSettings.potbank = constrain(potSettings.potbank + amtSlow, 0, NUM_CC_BANKS - 1);
+ }
+ break;
+ case GPARAM_SCALE_ROOT:
+ {
+ if (musicScale != nullptr)
+ {
+ int prevRoot = scaleConfig.scaleRoot;
+ scaleConfig.scaleRoot = constrain(scaleConfig.scaleRoot + amtSlow, 0, 12 - 1);
+ if (prevRoot != scaleConfig.scaleRoot)
+ {
+ musicScale->calculateScale(scaleConfig.scaleRoot, scaleConfig.scalePattern);
+ }
+ }
+ }
+ break;
+ case GPARAM_SCALE_PAT:
+ {
+ if (musicScale != nullptr)
+ {
+ int prevPat = scaleConfig.scalePattern;
+ scaleConfig.scalePattern = constrain(scaleConfig.scalePattern + amtSlow, -1, musicScale->getNumScales() - 1);
+ if (prevPat != scaleConfig.scalePattern)
+ {
+ omxDisp.displayMessage(musicScale->getScaleName(scaleConfig.scalePattern));
+ musicScale->calculateScale(scaleConfig.scaleRoot, scaleConfig.scalePattern);
+ }
+ }
+ }
+ break;
+ case GPARAM_SCALE_LOCK:
+ {
+ scaleConfig.lockScale = constrain(scaleConfig.lockScale + amtSlow, 0, 1);
+ }
+ break;
+ case GPARAM_SCALE_GRP16:
+ {
+ scaleConfig.group16 = constrain(scaleConfig.group16 + amtSlow, 0, 1);
+ }
+ break;
+ case GPARAM_MACRO_MODE:
+ {
+ midiMacroConfig.midiMacro = constrain(midiMacroConfig.midiMacro + amtSlow, 0, nummacromodes);
+ }
+ break;
+ case GPARAM_MACRO_CHAN:
+ {
+ midiMacroConfig.midiMacroChan = constrain(midiMacroConfig.midiMacroChan + amtSlow, 1, 16);
+ }
+ break;
+ case GPARAM_MIDI_LASTNOTE:
+ case GPARAM_MIDI_LASTVEL:
+ case GPARAM_POTS_LASTVAL:
+ case GPARAM_POTS_LASTCC:
+ {
+ Serial.println("Param not editable: ");
+ Serial.println(paramType);
+ }
+ break;
+ }
+}
+
+void OmxUtil::setupPageLegend(uint8_t index, uint8_t paramType)
+{
+ setupPageLegend(nullptr, index, paramType);
+}
+
+void OmxUtil::setupPageLegend(MusicScales *musicScale, uint8_t index, uint8_t paramType)
+{
+ switch (paramType)
+ {
+ case GPARAM_MOUT_OCT:
+ {
+ omxDisp.legends[index] = "OCT";
+ omxDisp.legendVals[index] = (int)midiSettings.octave + 4;
+ }
+ break;
+ case GPARAM_MOUT_CHAN:
+ {
+ omxDisp.legends[index] = "CH";
+ omxDisp.legendVals[index] = sysSettings.midiChannel;
+ }
+ break;
+ case GPARAM_MOUT_VEL:
+ {
+ omxDisp.legends[index] = "VEL";
+ omxDisp.legendVals[index] = midiSettings.defaultVelocity;
+ }
+ break;
+ case GPARAM_MIDI_THRU:
+ {
+ omxDisp.legends[index] = "THRU"; // MIDI thru (usb to hardware)
+ omxDisp.legendText[index] = midiSettings.midiSoftThru ? "On" : "Off";
+ }
+ break;
+ case GPARAM_MIDI_LASTNOTE:
+ {
+ omxDisp.legends[index] = "NOTE";
+ omxDisp.legendVals[index] = midiSettings.midiLastNote;
+ }
+ break;
+ case GPARAM_MIDI_LASTVEL:
+ {
+ omxDisp.legends[index] = "VEL";
+ omxDisp.legendVals[index] = midiSettings.midiLastVel;
+ }
+ break;
+ case GPARAM_POTS_LASTCC:
+ {
+ omxDisp.legends[index] = "P CC";
+ omxDisp.legendVals[index] = potSettings.potCC;
+ }
+ break;
+ case GPARAM_POTS_LASTVAL:
+ {
+ omxDisp.legends[index] = "P VAL";
+ omxDisp.legendVals[index] = potSettings.potVal;
+ }
+ break;
+ case GPARAM_POTS_PBANK:
+ {
+ omxDisp.legends[index] = "PBNK"; // Potentiometer Banks
+ omxDisp.legendVals[index] = potSettings.potbank + 1;
+ }
+ break;
+ case GPARAM_SCALE_ROOT:
+ {
+ if(musicScale != nullptr)
+ {
+ omxDisp.legends[index] = "ROOT";
+ omxDisp.legendText[index] = musicScale->getNoteName(scaleConfig.scaleRoot);
+ }
+ }
+ break;
+ case GPARAM_SCALE_PAT:
+ {
+ omxDisp.legends[index] = "SCALE";
+
+ if (scaleConfig.scalePattern < 0)
+ {
+ omxDisp.legendText[index] = "CHRM";
+ }
+ else
+ {
+ omxDisp.legendVals[index] = scaleConfig.scalePattern;
+ }
+ }
+ break;
+ case GPARAM_SCALE_LOCK:
+ {
+ omxDisp.legends[index] = "LOCK";
+ omxDisp.legendText[index] = scaleConfig.lockScale ? "On" : "Off";
+ }
+ break;
+ case GPARAM_SCALE_GRP16:
+ {
+ omxDisp.legends[index] = "GROUP";
+ omxDisp.legendText[index] = scaleConfig.group16 ? "On" : "Off";
+ }
+ break;
+ case GPARAM_MACRO_MODE:
+ {
+ omxDisp.legends[index] = "MCRO"; // Macro mode
+ omxDisp.legendText[index] = macromodes[midiMacroConfig.midiMacro];
+ }
+ break;
+ case GPARAM_MACRO_CHAN:
+ {
+ omxDisp.legends[index] = "M-CH";
+ omxDisp.legendVals[index] = midiMacroConfig.midiMacroChan;
+ }
+ break;
+ }
+}
+
+OmxUtil omxUtil;
diff --git a/Archive/OMX-27-firmware/src/utils/omx_util.h b/Archive/OMX-27-firmware/src/utils/omx_util.h
new file mode 100644
index 00000000..40ed945d
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/utils/omx_util.h
@@ -0,0 +1,101 @@
+#pragma once
+#include "../config.h"
+#include "../utils/music_scales.h"
+#include "../modes/omx_mode_interface.h"
+#include "../modes/submodes/submode_clearstorage.h"
+
+enum GlobalParams
+{
+ GPARAM_MOUT_OCT,
+ GPARAM_MOUT_CHAN,
+ GPARAM_MOUT_VEL,
+ GPARAM_MIDI_THRU,
+ GPARAM_MIDI_LASTNOTE,
+ GPARAM_MIDI_LASTVEL,
+ GPARAM_POTS_LASTVAL,
+ GPARAM_POTS_LASTCC,
+ GPARAM_POTS_PBANK,
+ GPARAM_SCALE_ROOT,
+ GPARAM_SCALE_PAT,
+ GPARAM_SCALE_LOCK,
+ GPARAM_SCALE_GRP16,
+ GPARAM_MACRO_MODE,
+ GPARAM_MACRO_CHAN,
+ GPARAM_CLOCK_SOURCE,
+ GPARAM_CLOCK_SEND
+};
+
+class OmxUtil
+{
+public:
+ OmxUtil()
+ {
+ }
+
+ void setup();
+
+ void sendPots(int val, int channel);
+
+ // random float between 0.0 to 1.0, inclusive
+ static float randFloat();
+
+ // Assumes b is greater than a
+ static float lerp(float a, float b, float t);
+
+ // #### Clocks, might want to put in own class
+ void advanceClock(OmxModeInterface *activeOmxMode, Micros advance);
+ void advanceSteps(Micros advance);
+ void setGlobalSwing(int swng_amt);
+ void resetPPQCounter();
+ void resetClocks();
+ void restartClocks();
+
+ void startClocks();
+ void resumeClocks();
+ void stopClocks();
+
+ bool areClocksRunning();
+
+ // #### Outbound CV note on/off
+ // void cvNoteOn(uint8_t notenum);
+ // void cvNoteOff(uint8_t notenum);
+
+ // #### Outbound MIDI note on/off
+ void midiNoteOn(int notenum, int velocity, int channel);
+ void midiNoteOn(MusicScales *scale, int notenum, int velocity, int channel);
+ void midiNoteOff(int notenum, int channel);
+
+ void allOff();
+
+ MidiNoteGroup midiNoteOn2(MusicScales *scale, int notenum, int velocity, int channel);
+ MidiNoteGroup midiNoteOff2(int notenum, int channel);
+
+ MidiNoteGroup midiDrumNoteOn(uint8_t keyIndex, uint8_t notenum, int velocity, int channel);
+ MidiNoteGroup midiDrumNoteOff(uint8_t keyIndex);
+
+ // Used for global params defined in GlobalParams to avoid code duplication
+ // called on Encoder update to edit a parameter
+ void onEncoderChangedEditParam(Encoder::Update *enc, uint8_t selectedParmIndex, uint8_t targetParamIndex, uint8_t paramType);
+ void onEncoderChangedEditParam(Encoder::Update *enc, MusicScales *musicScale, uint8_t selectedParmIndex, uint8_t targetParamIndex, uint8_t paramType);
+
+ // Used for global page legends defined in GlobalParams to avoid code duplication
+ void setupPageLegend(uint8_t index, uint8_t paramType);
+ void setupPageLegend(MusicScales *musicScale, uint8_t index, uint8_t paramType);
+
+ SubModeClearStorage subModeClearStorage;
+
+private:
+ // int potbank = 0;
+ // int analogValues[5] = {0,0,0,0,0}; // default values
+ // int potValues[5] = {0,0,0,0,0};
+ // int potCC = pots[potbank][0];
+ // int potVal = analogValues[0];
+
+ // signed to avoid rollover
+ signed long long timeToNextClock = 0;
+
+ bool sendClocks_ = true;
+ OmxModeInterface *activeOmxMode_;
+};
+
+extern OmxUtil omxUtil;
diff --git a/Archive/OMX-27-firmware/src/utils/param_manager.cpp b/Archive/OMX-27-firmware/src/utils/param_manager.cpp
new file mode 100644
index 00000000..b4623cdc
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/utils/param_manager.cpp
@@ -0,0 +1,264 @@
+#include "param_manager.h"
+
+// Max 10 pages
+int8_t ParamManager::addPage(uint8_t numberOfParams)
+{
+ if (numberOfPages >= kMaxPages)
+ return -1;
+
+ uint8_t newPageIndex = numberOfPages;
+ pageConfigs[newPageIndex].numberOfParams = numberOfParams;
+ pageConfigs[newPageIndex].enabled = true;
+ numberOfPages = numberOfPages + 1;
+ return newPageIndex;
+}
+
+int8_t ParamManager::addPages(uint8_t numberOfPages)
+{
+ int8_t result = -1;
+
+ for(uint8_t i = 0; i < numberOfPages; i++)
+ {
+ result = addPage(4);
+ if(result < 0)
+ {
+ return result;
+ }
+ }
+
+ return result;
+}
+
+void ParamManager::setPageEnabled(uint8_t pageIndex, bool enablePage)
+{
+ if (pageIndex < 0 || pageIndex > numberOfPages)
+ return;
+
+ pageConfigs[pageIndex].enabled = enablePage;
+
+ if (!enablePage && selectedPage == pageIndex)
+ {
+ for (int8_t i = pageIndex - 1; i >= 0; i--)
+ {
+ if (pageConfigs[i].enabled)
+ {
+ selectedPage = i;
+ return;
+ }
+ }
+
+ for (int8_t i = pageIndex + 1; i < numberOfPages; i++)
+ {
+ if (pageConfigs[i].enabled)
+ {
+ selectedPage = i;
+ return;
+ }
+ }
+ }
+}
+
+void ParamManager::changeParam(int8_t direction)
+{
+ if (direction == 0)
+ return;
+ if (direction > 0)
+ incrementParam();
+ else
+ decrementParam();
+}
+
+bool ParamManager::isFirstPage(int8_t pageIndex)
+{
+ if (pageIndex == 0)
+ return true;
+
+ for (int8_t i = pageIndex - 1; i >= 0; i--)
+ {
+ if (pageConfigs[i].enabled)
+ {
+ return false;
+ }
+ }
+ return true; // no pages ahead of this one found
+}
+
+bool ParamManager::isLastPage(int8_t pageIndex)
+{
+ if (pageIndex == numberOfPages - 1)
+ return true;
+
+ for (int8_t i = pageIndex + 1; i < numberOfPages; i++)
+ {
+ if (pageConfigs[i].enabled)
+ {
+ return false;
+ }
+ }
+ return true;
+}
+
+void ParamManager::incrementParam()
+{
+ if (numberOfPages == 0)
+ return;
+
+ selectedParam++;
+ if (selectedParam >= pageConfigs[selectedPage].numberOfParams)
+ {
+ if (rollPages || !isLastPage(selectedPage)) // Roll unless last page or roll pages
+ {
+ selectedParam = 0;
+ }
+ else
+ {
+ selectedParam = max(min(selectedParam - 1, pageConfigs[selectedPage].numberOfParams - 1), 0);
+ }
+
+ if (!lockSelectedPage)
+ {
+ incrementPage();
+ selectedParam = constrain(selectedParam, 0, pageConfigs[selectedPage].numberOfParams - 1);
+ }
+ }
+}
+void ParamManager::decrementParam()
+{
+ if (numberOfPages == 0)
+ return;
+
+ selectedParam--;
+ if (selectedParam < 0)
+ {
+ if (rollPages || !isFirstPage(selectedPage)) // Roll unless first page or roll pages
+ {
+ selectedParam = max(pageConfigs[selectedPage].numberOfParams - 1, 0);
+ }
+ else
+ {
+ selectedParam = 0;
+ }
+
+ if (!lockSelectedPage)
+ {
+ decrementPage();
+
+ selectedParam = constrain(selectedParam, 0, pageConfigs[selectedPage].numberOfParams - 1);
+ }
+ }
+}
+
+void ParamManager::incrementPage()
+{
+ if (numberOfPages == 0)
+ return;
+
+ bool foundEnabledPage = false;
+
+ for (int8_t i = selectedPage + 1; i < numberOfPages; i++)
+ {
+ if (pageConfigs[i].enabled)
+ {
+ selectedPage = i;
+ foundEnabledPage = true;
+ break;
+ }
+ }
+
+ if (!foundEnabledPage)
+ {
+ selectedPage = selectedPage + 1;
+ }
+
+ if (selectedPage >= numberOfPages)
+ {
+ if (rollPages)
+ {
+ selectedPage = 0;
+ }
+ else
+ {
+ selectedPage = max(selectedPage - 1, 0);
+ }
+ }
+}
+void ParamManager::decrementPage()
+{
+ if (numberOfPages == 0)
+ return;
+
+ bool foundEnabledPage = false;
+
+ for (int8_t i = selectedPage - 1; i >= 0; i--)
+ {
+ if (pageConfigs[i].enabled)
+ {
+ selectedPage = i;
+ foundEnabledPage = true;
+ break;
+ }
+ }
+
+ if (!foundEnabledPage)
+ {
+ selectedPage = selectedPage - 1;
+ }
+
+ if (selectedPage < 0)
+ {
+ if (rollPages)
+ {
+ selectedPage = numberOfPages - 1;
+ }
+ else
+ {
+ selectedPage = min(selectedPage + 1, numberOfPages - 1);
+ }
+ }
+}
+
+int8_t ParamManager::getSelPage()
+{
+ return selectedPage;
+}
+
+void ParamManager::setSelPage(int8_t newPage)
+{
+ if (newPage < 0 || newPage >= numberOfPages)
+ return;
+ selectedPage = newPage;
+}
+
+void ParamManager::setSelPageAndParam(int8_t newPage, int8_t newParam)
+{
+ setSelPage(newPage);
+ setSelParam(newParam);
+}
+
+int8_t ParamManager::getSelParam()
+{
+ return selectedParam;
+}
+
+void ParamManager::setSelParam(int8_t newParam)
+{
+ if (numberOfPages == 0)
+ return;
+ if (newParam < 0 || newParam >= pageConfigs[selectedPage].numberOfParams)
+ return;
+
+ selectedParam = newParam;
+}
+
+uint8_t ParamManager::getNumPages()
+{
+ return numberOfPages;
+}
+
+uint8_t ParamManager::getNumOfParamsForPage(uint8_t pageIndex)
+{
+ if (numberOfPages == 0 || pageIndex < 0 || pageIndex >= numberOfPages)
+ return 0;
+
+ return pageConfigs[pageIndex].numberOfParams;
+}
diff --git a/Archive/OMX-27-firmware/src/utils/param_manager.h b/Archive/OMX-27-firmware/src/utils/param_manager.h
new file mode 100644
index 00000000..1763329e
--- /dev/null
+++ b/Archive/OMX-27-firmware/src/utils/param_manager.h
@@ -0,0 +1,55 @@
+#pragma once
+#include
+
+class ParamManager
+{
+public:
+ static const int kMaxPages = 10;
+
+ // If true page will loop back to first after going past last page
+ // If false, page will not increment
+ bool rollPages = false;
+
+ // If true the current page will be locked. Incrementing or decrementing params will loop on current page
+ bool lockSelectedPage = false;
+
+ // Max 10 pages, returns index of new page. returns -1 if can't add
+ int8_t addPage(uint8_t numberOfParams = 4);
+
+ // Adds multiple pages with 4 params each, returns -1 if pages could not be added
+ int8_t addPages(uint8_t numberOfPages);
+
+ // Increment or decrement based on direction
+ // < 0 == CCW > 0 == CW 0 = do nothing
+ void changeParam(int8_t direction);
+ void setPageEnabled(uint8_t pageIndex, bool enablePage);
+ void incrementParam();
+ void decrementParam();
+
+ void incrementPage();
+ void decrementPage();
+
+ int8_t getSelPage();
+ void setSelPage(int8_t newPage);
+ void setSelPageAndParam(int8_t newPage, int8_t newParam);
+
+ int8_t getSelParam();
+ void setSelParam(int8_t newParam);
+ uint8_t getNumPages();
+ uint8_t getNumOfParamsForPage(uint8_t pageIndex);
+
+private:
+ struct PageConfig
+ {
+ uint8_t numberOfParams : 6;
+ bool enabled;
+ };
+
+ int8_t selectedPage = 0;
+ int8_t selectedParam = 0;
+ uint8_t numberOfPages = 0;
+ PageConfig pageConfigs[kMaxPages];
+
+ bool isFirstPage(int8_t pageIndex);
+ bool isLastPage(int8_t pageIndex);
+};
diff --git a/OMX-27-firmware/usb_names.c b/Archive/OMX-27-firmware/usb_names.c
similarity index 100%
rename from OMX-27-firmware/usb_names.c
rename to Archive/OMX-27-firmware/usb_names.c
diff --git a/Archive/OMX27_CheatSheet.pdf b/Archive/OMX27_CheatSheet.pdf
new file mode 100644
index 00000000..f6a36d66
Binary files /dev/null and b/Archive/OMX27_CheatSheet.pdf differ
diff --git a/Archive/README.md b/Archive/README.md
new file mode 100644
index 00000000..d7c450f6
--- /dev/null
+++ b/Archive/README.md
@@ -0,0 +1,128 @@
+# OMX-27 HARDWARE VERSIONS 1 and 2
+
+Mechanical key switch midi keyboard and sequencer. Based on Teensy 3.2 and Cherry MX RGB key switches.
+
+Full kits and partial kits are [available for sale here](https://www.denki-oto.com/).
+
+Dimensions: 313mm x 65mm
+
+## Firmware
+
+
+Kits are shipped with a blank Teensy. You will need to flash the firmware to the device.
+
+### Load pre-compiled firmware w/ TyUpdater
+
+Download the correct OMX-27 firmware "hex" file from the [GitHub Releases page](https://github.com/okyeron/OMX-27/releases) page or the Firmware-Hexes directory in this repo.
+
+NOTE - 2023 boards with Teensy 4.0 have a different firmware and will have a "T4" suffix.
+
+Get TyTools [from GitHub here](https://github.com/Koromix/tytools/releases). More info here (https://koromix.dev/tytools).
+
+Copy TyUploader to your machine and open it. Be sure your OMX-27 is plugged in. It should show up in the TyUpdater application.
+
+
+
+Click the Upload button and select the firmware hex file you want to upload. This should upload the firmware and the OMX-27 should reboot. That's it.
+
+
+### Teensyduino (compile yourself)
+
+Install Teensyduino from the [PJRC website](https://www.pjrc.com/teensy/teensyduino.html).
+
+In Teensyduino Library Manager - check to be sure these are installed and on the most recent versions.
+
+__Libraries:__
+Adafruit_Keypad
+Adafruit_NeoPixel
+Adafruit_SSD1306
+Adafruit_GFX_Library
+U8g2_for_Adafruit_GFX
+Adafruit_FRAM_I2C
+Adafruit_MCP4725
+
+
+
+
+Also check to be sure MIDI Library (by Francois Best / fortyseveneffects) is updated to 5.02 (I believe this is installed by default with Teensyduino)
+
+Set the following for the Teensy under the Tools menu:
+
+__Board: Teensy 3.2/3.1__ or __Board: Teensy 4.0__
+__USB Type: MIDI__
+__CPU Speed: 120 MHz (overclock)__
+
+Open the sketch at `OMX-27-firmware/OMX-27-firmware.ino`, click verify to ensure it all compiles and upload to flash the firmware to the hardware, pushing the button on the Teensy first.
+
+### PlatformIO / VSCode (optional)
+
+Ensure Homebrew in installed. [Instructions](https://brew.sh/)
+Install PlatformIO CLI tools. [Detailed Instructions](https://platformio.org/install/cli)
+
+```sh
+# Mac OSX
+brew install platformio
+
+# check out the project
+git checkout https://github.com/okyeron/OMX-27.git
+
+# go to the project directory
+cd OMX-27
+
+# compile the project (this may take a while the first time)
+pio run
+
+# upload to hardware (don't forget to push button on Teensy)
+pio run -t upload
+
+# use serial monitor for debugging
+pio device monitor
+
+# clear FRAM/EEPROM
+pio run -t clear-storage
+```
+
+(optional) Install PlatformIO IDE VSCode extension. [Instructions](https://platformio.org/platformio-ide)
+
+Install EditorConfig extension for your text editor. [Instructions](https://editorconfig.org/)
+
+Note: when making changes using the PlatformIO toolchain, please ensure the sketch still builds on Teensyduino before opening a PR.
+
+## BOM
+
+[Bill of Materials]()
+
+## Build
+
+[Build Guide]()
+
+## Docs
+
+[Documentation]()
+
+## Web Configurator
+
+[Online Configurator](https://okyeron.github.io/OMX-27/webconfig/index.html)
+
+## FAQ
+
+Q: What key switches are recommended?
+A: Any RGB switches with a Cherry MX footprint can be used - I'm using Cherry MX RGB and these are linked in the [BOM](). Different varieties are available (Red, Brown, etc.)
+
+Q: Can I use other key switches?
+A: Yes - as long as they have the same footprint as Cherry MX switches and a window/opening for the LED to shine through. Low profile keys like the Cherry Low Profile or Kailh Choc switches have a different footprint and will not work.
+
+Q: What about recommended Keycaps?
+A: Also listed in the [BOM](). You want an MX stem cap, with translucency or a window for the LED to shine through. DSA profile caps work well.
+
+Q: Does this project require soldering?
+A: Yes. Thru-hole soldering is required along with some easy SMD (LEDs and jacks).
+
+Q: What's with these LEDs?
+A: This project uses SK6812-MINI-E reverse mount LEDs. They are somewhat hard to find, so I'll try to offer them included with kits. They are easy to solder, even if you've not done much SMD.
+
+Q: Can I get the Gerbers or order the pcbs myself?
+A: No. Not open source at this time.
+
+Q: Can I get some of those windowed keycaps you're using?
+A: Yes (send me an email).
diff --git a/Archive/build/BOM.md b/Archive/build/BOM.md
new file mode 100644
index 00000000..e52ae2c6
--- /dev/null
+++ b/Archive/build/BOM.md
@@ -0,0 +1,96 @@
+# OMX-27 BOM
+
+
+| Mouser | QTY | Part | Value | Package |
+|-----|:--:|-----|-----|-----|
+|[RC0805FR-1047RL](http://www.mouser.com/Search/ProductDetail.aspx?R=RC0805FR-1047RL)|2|R7 R8|47R|0805|
+|[603-RC0805FR-0710KL](http://www.mouser.com/Search/ProductDetail.aspx?R=603-RC0805FR-0710KL)|2|R1 R2|10K|0805|
+|[652-CR0805-FX2202ELF](http://www.mouser.com/Search/ProductDetail.aspx?R=652-CR0805-FX2202ELF)|2|R4 R6|22K|0805|
+|[RC0805FR-1056KL](http://www.mouser.com/Search/ProductDetail.aspx?R=RC0805FR-1056KL)|2|R3 R5|56K|0805|
+|[80-C0805C104J5RACLR](http://www.mouser.com/Search/ProductDetail.aspx?R=80-C0805C104J5RACLR)|28|C1-C29|100nF|0805|
+|[710-885382207006](http://www.mouser.com/Search/ProductDetail.aspx?R=710-885382207006)|2|C30, C31|10nF|0805|
+|[621-1N4148W-F](http://www.mouser.com/Search/ProductDetail.aspx?R=621-1N4148W-F)|27|D1-D27|1N4148 Diode|SOD-123|
+|[SJ-3523-SMT-TR](http://www.mouser.com/Search/ProductDetail.aspx?R=SJ-3523-SMT-TR)|1|J1|SJ-3523-SMT-TR|3.5 mm jack stereo|
+|[490-MJ-3523-SMT-TR](http://www.mouser.com/Search/ProductDetail.aspx?R=490-MJ-3523-SMT-TR)|2|J2,J3|MJ-3523-SMT|3.5 mm jack mono|
+|[540-MX3A-L1NA](http://www.mouser.com/Search/ProductDetail.aspx?R=540-MX3A-L1NA)|27|K1-K27|CHERRY-MX|CHERRY-MX RGB Silent Red \*|
+|[595-TLV9062IDR](http://www.mouser.com/Search/ProductDetail.aspx?R=595-TLV9062IDR)|1|U1|SOIC127P600X175-8N|TLV9062IDR|
+|[AYZ0202AGRLC](http://www.mouser.com/Search/ProductDetail.aspx?R=AYZ0202AGRLC)|1|S1|DPDT Switch|SWITCH-DPDT-SMD-AYZ0202|
+|[688-RK09K1130A5R](http://www.mouser.com/Search/ProductDetail.aspx?R=688-RK09K1130A5R)|5|VR1-VR4,VR6|10K|9MM_SNAP-IN_POT*|
+|[652-PEC11R-4015F-S24](http://www.mouser.com/Search/ProductDetail.aspx?R=652-PEC11R-4015F-S24)|1|VR5|PEC11+SWITCH|Encoder with Switch|
+| [aliexpress](https://www.aliexpress.com/item/4000475685852.html?spm=a2g0s.9042311.0.0.601b4c4dcyhOZn) / [ebay](https://www.ebay.com/itm/100-2000pcs-SK6812-MINI-E-LED-CHIP-SK6812-3228-4pin-dream-color-LEDS-DC5V/224140435419?hash=item342fcf9fdb:g:XbAAAOSwzkRd8g96)|27|LED1-LED27|SK6812MINIE|SK6812-MINI-E|
+| [PJRC Store](https://www.pjrc.com/store/teensy32.html) |1| |TEENSY 3.2||
+| |1| |OLED - 128x32 I2C display| \**See below|
+| | | |header pins| \***See below|
+
+\* POTS - I used trimmer type pots because they're a little more low profile. But you can use alpha pots or whatever you have around.
+
+\** OLED - 128x32 I2C display (SSD1306) with pin order ( GND, VCC, SCL, SDA )
+example from eBay:
+"0.91" 128x32 IIC I2C White OLED LCD Display DIY Module For Arduino"
+https://www.ebay.com/itm/293660021494
+
+\*** Headers:
+1X04 (oled)
+1x14 x 2 (teensy)
+1x01 (teensy dac pin)
+
+TIP: Get 1x40 breakaway headers and cut what you need.
+
+[Mouser Cart (work in progress)](https://www.mouser.com/ProjectManager/ProjectDetail.aspx?AccessID=13c0107d30) - __DOES NOT include Teensy, OLED, LEDs or headers__
+
+Mounting Hardware is NOT LISTED
+
+Knobs are up to you.
+
+---
+### \* Key switches:
+
+Any of the Cherry MX RGB switches will work. Red/SilentRed/Blue/Brown/Black/SpeedSilver.
+Red is linear (45g). Blue is clicky & tactile (50g). Brown is tactile (45g). Black is similar to Red but 60g actuation force. SpeedSilver (shorter key travel for gamers) are linear and __Expensive__( 45g)
+
+Cherry MX RGB part numbers:
+
+| name | part num | type | actuation |
+|-----|----|-----|----|
+|SilentRed |[MX3A-L1NA](https://www.mouser.com/ProductDetail/CHERRY/MX3A-L1NA/?qs=F5EMLAvA7IA6PAS7ry3I9w%3D%3D)| linear | (45g) |
+|Red |MX1A-L1NA| linear | (45g) |
+|SilentBlack |[MX3A-11NA](https://www.mouser.com/ProductDetail/CHERRY/MX3A-11NA/?qs=F5EMLAvA7ICizK1XKjfN9w%3D%3D)| linear | (60g) |
+|Black |MX1A-11NA| linear | (60g) |
+|Brown |[MX1A-G1NA](https://www.mouser.com/ProductDetail/540-MX1A-G1NA/)| tactile | (45g) |
+|Blue |MX1A-E1NA| clicky & tactile | (50g) |
+|Silver |[MX1A-51NA](https://www.mouser.com/ProductDetail/CHERRY/MX1A-51NA/?qs=F5EMLAvA7IB4ByA0zXdBkg%3D%3D)| linear | (45g) |
+
+Reference: https://www.mouser.com/pdfDocs/cherrykeyswitches.pdf
+
+
+### Keycaps:
+
+Any MX-compatible keycaps will work, but you'll want one designed for backlighting, such as a "backlit two-shot", "translucent", "shine-through", or "windowed".
+
+I like the DSA profile caps for this application.
+
+[DSA "Dolch" Keyset (Two Shot) "Windowed" Keys](https://pimpmykeyboard.com/dsa-dolch-keyset-two-shot/) (choose the __LED Kit__ option).
+These come in a pack of 4 keycaps. You will need 7 packs (@ ~$70 total).
+
+[Flashquark Translucent DSA Keycaps](https://flashquark.com/product/translucent-dsa-keycaps/) (in Black, White, Clear, Blue, Red) - __50 per pack__. ($12.99-$15.99 per pack)
+
+[Blue Hat 1U DSA Blank Printed Keycaps PBT Keycaps](https://www.amazon.com/gp/product/B07SJKMNWC) (Gray Translucent) - __37 per pack__. ($15.55 per pack and prime shipping)
+(about the same as the black ones from Flashquark above)
+
+[Maxkeyboard Black Translucent MX Blank](https://www.maxkeyboard.com/black-translucent-cherry-mx-blank-keycap-set-for-esc-w-a-s-d-or-e-s-d-f-and-arrow-keys.html) (pack of 9). ($21 for 3 packs)
+
+### Knobs
+
+So many opinions about knobs.
+
+Micro knobs (Befaco Style) can be found at these places:
+https://www.thonk.co.uk/shop/tall-trimmer-toppers/
+https://www.thonk.co.uk/shop/micro-knobs/
+https://www.thonk.co.uk/shop/tall-trimmer-toppers/
+
+
+### USB Cable:
+
+I recommend using a right angle extension cable [like this one from Amazon](https://www.amazon.com/gp/product/B015PSU5F6/)
+
+Be sure you have a good, known working, USB DATA CABLE and not just a charging cable.
diff --git a/Archive/build/Build-Kit.md b/Archive/build/Build-Kit.md
new file mode 100644
index 00000000..a3adb5a7
--- /dev/null
+++ b/Archive/build/Build-Kit.md
@@ -0,0 +1,251 @@
+# OMX-27
+
+
+
+
+# Before you start
+
+## READ THIS ENTIRE GUIDE FIRST
+
+Also - see these __Build Videos:__
+
+[Part 1 - LEDs](https://youtu.be/UFm8Dfpjoz4)
+[Part 2 - Teensy](https://youtu.be/W-rJqxFzsLw) (Not yet updated for Teensy 4.0)
+[Part 3- Pots and Testing](https://youtu.be/rtUBW4xm9us)
+[Part 4 - Switches and Assembly](https://youtu.be/jUWWuaacoz4)
+
+The key-switches are going to be the VERY LAST thing you solder. __After you solder the switches in, everything on the inside is going to be inaccessible.__
+
+Ideally you want to be able to test all the LEDs, the OLED, and the pots/encoder before putting the switches on.
+
+I'd also suggest testing each switch connection with a piece of wire or tweezers so you can confirm the diodes/LEDs/caps are all soldered correctly.
+
+Follow the order of operations here to make your life easier. __NOTE - the keyswitches are absolutely the last thing you solder.__ Make sure everything else looks good before you do the switches.
+
+Also important - Keyswitches are snapped into the keyplate first (before soldering them).
+
+Don't forget to put the spacer layer in-between the main PCB and the keyplate before you solder all the switches.
+
+
+### Soldering Tips
+
+I work with a fine point tip on my iron at 400C. With this setup I typically hold the iron on a pad for about 2 seconds and then apply a bit of solder and then hold the iron there for anything 2-3 seconds. You want to watch for the solder to flow around the joint, but not to hold the iron there forever.
+
+See [Adafruit's guide to excellent soldering](https://learn.adafruit.com/adafruit-guide-excellent-soldering) for lots of good tips and tricks.
+
+Nice to have tools:
+ - flush diagonal cutters
+ - tweezers
+
+
+---
+
+# Build from Kit
+
+### LEDs
+
+The LEDs are __Reverse Mount__ and are soldered to the back-side of the PCB with the LED facing towards the top of the PCB. When looking at the back of the PCB as in the picture, the GND leg is the top right pad for each one (marked with a red triangle in the picture below). The LED itself has a "notched" leg for GND.
+
+
+
+Set each LED into position (tweezers are handy for this) and __then double check the ground pin is in the right position__.
+
+
+
+
+
+Solder/tack the bottom right corner pad of each LED to hold each one in place. Then check the orientation of each LED to be sure they're nice and square in the hole. If not, warm up the solder there and reposition as needed.
+
+After you're happy with the LEDs being in the proper positions - solder the rest of the pads.
+
+### TEENSY
+
+For the keyplate to fit properly, the Teensy MUST be flush-mounted to the top of the main PCB.
+
+(Teensy 3.2 and PCB v1.5 only) An insulating kapton spacer is included with your kit . Use this between the bottom of the teensy and the main PCB to reduce the chances of unintended shorts.
+
+See below for Teensy 4.0 instructions (2023 v2.0 boards).
+
+__Teensy 3.2 jig__
+
+Use the included acrylic jig to set up your Teensy like the following for soldering.
+
+Short side of the headers goes down to the jig and the long side up.
+
+
+
+Add a 1x3 and 1x1 in the appropriate places. The 1x1 directly next to the 1x3 is not connected to anything so you can solder that or not (your choice).
+
+
+
+
+Add the two spacers (maybe even tape those two together so they don't wiggle around.
+
+
+
+Drop the Teensy into place. There should just be a small amount of header sticking up from the Teensy at this point.
+
+
+
+DON'T SOLDER A HEADER TO THE VUSB PIN - it's not used. This is the 1x1 pin/hole right next to the USB jack on the Teensy (on the inside row).
+
+__Teensy 4.0 jig__
+
+The Teensy 4.0 version (board v2.0) only uses the 2 outer rows of pins. (4 less pins to solder!)
+
+Note the plastic parts of the jig have an etched out area - this is to allow space for the components on the underside of the Teensy 4.0.
+
+
+
+
+__Soldering__
+
+Solder the pins to the Teensy first.
+
+Then remove the jig and carefully remove the black plastic from the headers. __Hold onto the black spacers for the next step.__
+
+After you've removed the plastic, slide the thin yellow kapton spacer thingy onto the bottom of the teensy - this should end up between the teensy and the main board as an insulator. Then drop the Teensy onto the main board so it sits nice and flat.
+
+
+
+To keep the pins from wiggling around while soldering the bottom, either
+
+ * Put a big piece of tape over the whole teensy to keep it in place and to keep the pins from getting pushed out
+
+ * Or push the black plastic bits from the headers onto the pins to hold them in place while soldering
+
+ * Or both
+
+
+
+Flip the board over and solder the pins to the bottom. Try to tack/solder one pin on either side in place while pushing your finger against the teensy to make sure it's absolutely flat against the main pcb.
+
+Once you're happy with the flatness - solder the rest of the pins. Be careful not to push down on the pins while soldering.
+
+Using flush cutters, trim the pins away. Be careful not to nick/scratch the pcb.
+
+
+
+### OLED
+
+The OLED display sits on a regular header (not flush like the Teensy) the display should be close to level with the keyplate (the OLED glass will be about 0.5-1mm higher than the keyplate).
+
+__TIP:__ I suggest using a section of the header plastic you removed from the Teensy headers as a spacer to hold up the other side of the OLED PCB. Glue or tape a 1x4 chunk of the header plastic to the back of the OLED pcb and this will keep it level and support it while you solder (and after).
+
+
+
+
+Trim the headers on the top side of the OLED if you're worried about something shorting there.
+
+### JACKS, POTS, ENCODER, ETC.
+
+Snap pots and encoders into place and solder.
+
+You may need to gently squeeze the snap-in mounting pins together a tiny bit to get the pots to snap into place.
+
+
+
+---
+
+# __STOP HERE AND TEST THINGS__
+
+At this point you can flash the firmware and do some testing.
+
+See the instructions here (loading a HEX file) - https://llllllll.co/t/how-to-flash-the-firmware-on-a-teensy-micro-controller/20317 if you don't know how to flash firmware to a Teensy.
+
+The OLED should display something as soon as you plug into USB power.
+
+### LED test
+
+On startup all the LEDs should show a rainbow pattern.
+
+If your LEDs work up to a certain point (e.g. LEDs 1-7 work, LED 8-27 don't):
+
+- The problem is most likely a bad soldering joint on the erroneous LED itself, or on the LED that is RIGHT BEFORE this LED in the chain (in the above example, check LED 7 and 8). Carefully re-solder all connections again to fix the problem (melt the existing solder again, maybe apply some more, make sure it flows nicely between LED and PCB pad)
+
+- Check that the orientation of the LED is correct (see pictures above)
+
+
+
+### Switch contact test - AKA "the tweezer test"
+
+You will want to test the pads for each keyswitch on the PCB using tweezers or a piece of wire (a piece of wire will work much better than tweezers!). This is also a second check that the LED for that switch is working correctly.
+
+
+
+When you test the AUX key (top left-most key) - this will light up a total of 15 LEDs on the board. This is normal.
+
+
+
+If the LEDs do not light up for each switch contact, check the LEDs again first. A good test is to remove power and re-plug to see if the rainbow LED pattern shows on startup. If all the LEDs are working OK examine the diode adjacent to that switch position and be sure the soldering looks OK.
+
+Note - There are groups of Rows and Columns for sets of switches. If you get a group lighting up, it may be a corresponding pin on the Teensy for that row or column. Ask on Discord if you're stuck here.
+
+
+### MIDI test
+
+Use the [browser_test](../browser_test/index.html) script to show USB-MIDI input to your computer. Then you can check to be sure the pots are sending CCs and that you get MIDI note-ons/note-offs when you test each keyswitch's pads. Be sure you have the `oct` (octave) set to 4 on the display (change with encoder knob).
+
+Also test the Hardware MIDI 1/8" jack with an appropriate adapter and synth. Check the A/B switch position for your particular setup (try both to be sure you have the right one).
+
+---
+
+# Continue building
+
+### Acrylic Case Parts
+
+__Carefully__ remove the paper backing from the acrylic parts - the spacer and the back plate. Then set these aside for the next step.
+
+The spacer layer is pretty fragile - try not to break it. However, even if it does break, it might be fine since this sits in-between the other layers.
+
+
+### Key Switches
+
+Check the orientation of the switches. The pins go towards the bottom-half and the LED window at the top.
+
+
+
+Snap all the key-switches into the keyplate (from the top).
+
+
+
+The switches may be a tight fit. Be sure they are snapped all the way into place.
+
+Set the black acrylic spacer layer on the main PCB and align it around the various components. Then set the keyplate with switches into place to be sure all the pins line up and everything is nice and flat. You may need to gently bend key-switch pins into place if they got slightly bent in transport.
+
+
+
+
+Use the included case screws/nuts to fix everything together for soldering. I suggest using the holes down the middle of the case. This will ensure the key switches are held in place for soldering and that everything will remain flat.
+
+
+
+Solder all the switches.
+
+### Bottom Plate
+
+Then remove the screws/nuts and then reassemble with the bottom plate.
+
+The nuts fit into the captive cutouts on the bottom plate.
+
+
+
+
+
+### Teensy Cover
+
+Add the teensy cover plate with the two remaining screws/nuts.
+
+
+
+### Pot Knobs
+
+Push the knobs onto the pots, make sure the marking on the knob aligns with the marking on the pot.
+
+
+
+### Keycaps
+
+Then install the keycaps with the window on the top for the LEDs.
+
+
diff --git a/build/buildpix/OMX-27-bottom.png b/Archive/build/buildpix/OMX-27-bottom.png
similarity index 100%
rename from build/buildpix/OMX-27-bottom.png
rename to Archive/build/buildpix/OMX-27-bottom.png
diff --git a/build/buildpix/OMX-27-build-caps.png b/Archive/build/buildpix/OMX-27-build-caps.png
similarity index 100%
rename from build/buildpix/OMX-27-build-caps.png
rename to Archive/build/buildpix/OMX-27-build-caps.png
diff --git a/build/buildpix/OMX-27-build-diodes.png b/Archive/build/buildpix/OMX-27-build-diodes.png
similarity index 100%
rename from build/buildpix/OMX-27-build-diodes.png
rename to Archive/build/buildpix/OMX-27-build-diodes.png
diff --git a/build/buildpix/OMX-27-build-leds.png b/Archive/build/buildpix/OMX-27-build-leds.png
similarity index 100%
rename from build/buildpix/OMX-27-build-leds.png
rename to Archive/build/buildpix/OMX-27-build-leds.png
diff --git a/build/buildpix/OMX-27-build-resistors.png b/Archive/build/buildpix/OMX-27-build-resistors.png
similarity index 100%
rename from build/buildpix/OMX-27-build-resistors.png
rename to Archive/build/buildpix/OMX-27-build-resistors.png
diff --git a/build/buildpix/OMX-27-ic.png b/Archive/build/buildpix/OMX-27-ic.png
similarity index 100%
rename from build/buildpix/OMX-27-ic.png
rename to Archive/build/buildpix/OMX-27-ic.png
diff --git a/build/buildpix/OMX-27-top.png b/Archive/build/buildpix/OMX-27-top.png
similarity index 100%
rename from build/buildpix/OMX-27-top.png
rename to Archive/build/buildpix/OMX-27-top.png
diff --git a/build/buildpix/T4-jig.jpg b/Archive/build/buildpix/T4-jig.jpg
similarity index 100%
rename from build/buildpix/T4-jig.jpg
rename to Archive/build/buildpix/T4-jig.jpg
diff --git a/build/buildpix/T4-jig2.jpg b/Archive/build/buildpix/T4-jig2.jpg
similarity index 100%
rename from build/buildpix/T4-jig2.jpg
rename to Archive/build/buildpix/T4-jig2.jpg
diff --git a/build/buildpix/acrylic-spacer.jpg b/Archive/build/buildpix/acrylic-spacer.jpg
similarity index 100%
rename from build/buildpix/acrylic-spacer.jpg
rename to Archive/build/buildpix/acrylic-spacer.jpg
diff --git a/build/buildpix/bottom-plate-nocover.jpg b/Archive/build/buildpix/bottom-plate-nocover.jpg
similarity index 100%
rename from build/buildpix/bottom-plate-nocover.jpg
rename to Archive/build/buildpix/bottom-plate-nocover.jpg
diff --git a/build/buildpix/keycap-install.jpg b/Archive/build/buildpix/keycap-install.jpg
similarity index 100%
rename from build/buildpix/keycap-install.jpg
rename to Archive/build/buildpix/keycap-install.jpg
diff --git a/build/buildpix/keyswitch-back.jpg b/Archive/build/buildpix/keyswitch-back.jpg
similarity index 100%
rename from build/buildpix/keyswitch-back.jpg
rename to Archive/build/buildpix/keyswitch-back.jpg
diff --git a/build/buildpix/keyswitch-front.jpg b/Archive/build/buildpix/keyswitch-front.jpg
similarity index 100%
rename from build/buildpix/keyswitch-front.jpg
rename to Archive/build/buildpix/keyswitch-front.jpg
diff --git a/build/buildpix/keyswitch-single.jpg b/Archive/build/buildpix/keyswitch-single.jpg
similarity index 100%
rename from build/buildpix/keyswitch-single.jpg
rename to Archive/build/buildpix/keyswitch-single.jpg
diff --git a/build/buildpix/keyswitches-assemble-1.jpg b/Archive/build/buildpix/keyswitches-assemble-1.jpg
similarity index 100%
rename from build/buildpix/keyswitches-assemble-1.jpg
rename to Archive/build/buildpix/keyswitches-assemble-1.jpg
diff --git a/build/buildpix/keyswitches-assemble-2.jpg b/Archive/build/buildpix/keyswitches-assemble-2.jpg
similarity index 100%
rename from build/buildpix/keyswitches-assemble-2.jpg
rename to Archive/build/buildpix/keyswitches-assemble-2.jpg
diff --git a/build/buildpix/leds1.png b/Archive/build/buildpix/leds1.png
similarity index 100%
rename from build/buildpix/leds1.png
rename to Archive/build/buildpix/leds1.png
diff --git a/build/buildpix/leds2.png b/Archive/build/buildpix/leds2.png
similarity index 100%
rename from build/buildpix/leds2.png
rename to Archive/build/buildpix/leds2.png
diff --git a/build/buildpix/leds3.png b/Archive/build/buildpix/leds3.png
similarity index 100%
rename from build/buildpix/leds3.png
rename to Archive/build/buildpix/leds3.png
diff --git a/build/buildpix/oled-plastic.jpg b/Archive/build/buildpix/oled-plastic.jpg
similarity index 100%
rename from build/buildpix/oled-plastic.jpg
rename to Archive/build/buildpix/oled-plastic.jpg
diff --git a/build/buildpix/oled-soldered.jpg b/Archive/build/buildpix/oled-soldered.jpg
similarity index 100%
rename from build/buildpix/oled-soldered.jpg
rename to Archive/build/buildpix/oled-soldered.jpg
diff --git a/build/buildpix/pots-closeup.jpg b/Archive/build/buildpix/pots-closeup.jpg
similarity index 100%
rename from build/buildpix/pots-closeup.jpg
rename to Archive/build/buildpix/pots-closeup.jpg
diff --git a/build/buildpix/pots-soldered.jpg b/Archive/build/buildpix/pots-soldered.jpg
similarity index 100%
rename from build/buildpix/pots-soldered.jpg
rename to Archive/build/buildpix/pots-soldered.jpg
diff --git a/build/buildpix/startup-leds.jpg b/Archive/build/buildpix/startup-leds.jpg
similarity index 100%
rename from build/buildpix/startup-leds.jpg
rename to Archive/build/buildpix/startup-leds.jpg
diff --git a/build/buildpix/switch-test-aux.jpg b/Archive/build/buildpix/switch-test-aux.jpg
similarity index 100%
rename from build/buildpix/switch-test-aux.jpg
rename to Archive/build/buildpix/switch-test-aux.jpg
diff --git a/build/buildpix/switch-test.jpg b/Archive/build/buildpix/switch-test.jpg
similarity index 100%
rename from build/buildpix/switch-test.jpg
rename to Archive/build/buildpix/switch-test.jpg
diff --git a/build/buildpix/teensy-flush.jpg b/Archive/build/buildpix/teensy-flush.jpg
similarity index 100%
rename from build/buildpix/teensy-flush.jpg
rename to Archive/build/buildpix/teensy-flush.jpg
diff --git a/build/buildpix/teensy-reverse.jpg b/Archive/build/buildpix/teensy-reverse.jpg
similarity index 100%
rename from build/buildpix/teensy-reverse.jpg
rename to Archive/build/buildpix/teensy-reverse.jpg
diff --git a/build/buildpix/teensy-soldered.jpg b/Archive/build/buildpix/teensy-soldered.jpg
similarity index 100%
rename from build/buildpix/teensy-soldered.jpg
rename to Archive/build/buildpix/teensy-soldered.jpg
diff --git a/build/buildpix/teensy_jig_1.jpg b/Archive/build/buildpix/teensy_jig_1.jpg
similarity index 100%
rename from build/buildpix/teensy_jig_1.jpg
rename to Archive/build/buildpix/teensy_jig_1.jpg
diff --git a/build/buildpix/teensy_jig_2.jpg b/Archive/build/buildpix/teensy_jig_2.jpg
similarity index 100%
rename from build/buildpix/teensy_jig_2.jpg
rename to Archive/build/buildpix/teensy_jig_2.jpg
diff --git a/build/buildpix/teensy_jig_3.jpg b/Archive/build/buildpix/teensy_jig_3.jpg
similarity index 100%
rename from build/buildpix/teensy_jig_3.jpg
rename to Archive/build/buildpix/teensy_jig_3.jpg
diff --git a/build/buildpix/teensy_jig_4.jpg b/Archive/build/buildpix/teensy_jig_4.jpg
similarity index 100%
rename from build/buildpix/teensy_jig_4.jpg
rename to Archive/build/buildpix/teensy_jig_4.jpg
diff --git a/build/buildpix/teensy_jig_5.jpg b/Archive/build/buildpix/teensy_jig_5.jpg
similarity index 100%
rename from build/buildpix/teensy_jig_5.jpg
rename to Archive/build/buildpix/teensy_jig_5.jpg
diff --git a/build/buildpix/top-plate-cover.jpg b/Archive/build/buildpix/top-plate-cover.jpg
similarity index 100%
rename from build/buildpix/top-plate-cover.jpg
rename to Archive/build/buildpix/top-plate-cover.jpg
diff --git a/build/buildpix/top-plate-nocover.jpg b/Archive/build/buildpix/top-plate-nocover.jpg
similarity index 100%
rename from build/buildpix/top-plate-nocover.jpg
rename to Archive/build/buildpix/top-plate-nocover.jpg
diff --git a/changelog.md b/Archive/changelog.md
similarity index 100%
rename from changelog.md
rename to Archive/changelog.md
diff --git a/clear_storage/clear_storage.T32.hex b/Archive/clear_storage/clear_storage.T32.hex
similarity index 100%
rename from clear_storage/clear_storage.T32.hex
rename to Archive/clear_storage/clear_storage.T32.hex
diff --git a/clear_storage/clear_storage.T4.hex b/Archive/clear_storage/clear_storage.T4.hex
similarity index 100%
rename from clear_storage/clear_storage.T4.hex
rename to Archive/clear_storage/clear_storage.T4.hex
diff --git a/clear_storage/clear_storage.ino b/Archive/clear_storage/clear_storage.ino
similarity index 100%
rename from clear_storage/clear_storage.ino
rename to Archive/clear_storage/clear_storage.ino
diff --git a/clear_storage/register_storage_target.py b/Archive/clear_storage/register_storage_target.py
similarity index 100%
rename from clear_storage/register_storage_target.py
rename to Archive/clear_storage/register_storage_target.py
diff --git a/eeprom_clear/README.md b/Archive/eeprom_clear/README.md
similarity index 100%
rename from eeprom_clear/README.md
rename to Archive/eeprom_clear/README.md
diff --git a/eeprom_clear/eeprom_clear.hex b/Archive/eeprom_clear/eeprom_clear.hex
similarity index 100%
rename from eeprom_clear/eeprom_clear.hex
rename to Archive/eeprom_clear/eeprom_clear.hex
diff --git a/eeprom_clear/eeprom_clear.ino b/Archive/eeprom_clear/eeprom_clear.ino
similarity index 100%
rename from eeprom_clear/eeprom_clear.ino
rename to Archive/eeprom_clear/eeprom_clear.ino
diff --git a/Archive/images/omx27_layout2.png b/Archive/images/omx27_layout2.png
new file mode 100644
index 00000000..a4e48752
Binary files /dev/null and b/Archive/images/omx27_layout2.png differ
diff --git a/Archive/images/omx27_layout3.png b/Archive/images/omx27_layout3.png
new file mode 100644
index 00000000..1e96610e
Binary files /dev/null and b/Archive/images/omx27_layout3.png differ
diff --git a/Archive/images/omx27_layout4.png b/Archive/images/omx27_layout4.png
new file mode 100644
index 00000000..c4ef119c
Binary files /dev/null and b/Archive/images/omx27_layout4.png differ
diff --git a/Archive/images/omx27_m8macro.png b/Archive/images/omx27_m8macro.png
new file mode 100644
index 00000000..c984eaba
Binary files /dev/null and b/Archive/images/omx27_m8macro.png differ
diff --git a/images/tyupdater.png b/Archive/images/tyupdater.png
similarity index 100%
rename from images/tyupdater.png
rename to Archive/images/tyupdater.png
diff --git a/plates/OMX-27-bottom.ai b/Archive/plates/OMX-27-bottom.ai
similarity index 100%
rename from plates/OMX-27-bottom.ai
rename to Archive/plates/OMX-27-bottom.ai
diff --git a/plates/OMX-27-keyplate2.ai b/Archive/plates/OMX-27-keyplate2.ai
similarity index 100%
rename from plates/OMX-27-keyplate2.ai
rename to Archive/plates/OMX-27-keyplate2.ai
diff --git a/plates/OMX-27-keyplate3.ai b/Archive/plates/OMX-27-keyplate3.ai
similarity index 100%
rename from plates/OMX-27-keyplate3.ai
rename to Archive/plates/OMX-27-keyplate3.ai
diff --git a/plates/OMX-27-midplate-spacer.ai b/Archive/plates/OMX-27-midplate-spacer.ai
similarity index 100%
rename from plates/OMX-27-midplate-spacer.ai
rename to Archive/plates/OMX-27-midplate-spacer.ai
diff --git a/plates/OMX-27-outline.ai b/Archive/plates/OMX-27-outline.ai
similarity index 100%
rename from plates/OMX-27-outline.ai
rename to Archive/plates/OMX-27-outline.ai
diff --git a/plates/OMX-keyboard-layout.txt b/Archive/plates/OMX-keyboard-layout.txt
similarity index 100%
rename from plates/OMX-keyboard-layout.txt
rename to Archive/plates/OMX-keyboard-layout.txt
diff --git a/plates/OMX-plate-layout.txt b/Archive/plates/OMX-plate-layout.txt
similarity index 100%
rename from plates/OMX-plate-layout.txt
rename to Archive/plates/OMX-plate-layout.txt
diff --git a/plates/teensy3.2-flush-spacer.ai b/Archive/plates/teensy3.2-flush-spacer.ai
similarity index 100%
rename from plates/teensy3.2-flush-spacer.ai
rename to Archive/plates/teensy3.2-flush-spacer.ai
diff --git a/Archive/platformio.ini b/Archive/platformio.ini
new file mode 100644
index 00000000..9b8944a4
--- /dev/null
+++ b/Archive/platformio.ini
@@ -0,0 +1,42 @@
+; PlatformIO Project Configuration File
+;
+; Build options: build flags, source filter
+; Upload options: custom upload port, speed and extra flags
+; Library options: dependencies, extra library storages
+; Advanced options: extra scripting
+;
+; Please visit documentation for the other options and examples
+; https://docs.platformio.org/page/projectconf.html
+
+[platformio]
+src_dir = OMX-27-firmware
+
+[env]
+framework = arduino
+upload_protocol = teensy-cli
+monitor_speed = 115200
+lib_deps =
+ adafruit/Adafruit BusIO @ ^1.9.3
+ adafruit/Adafruit FRAM I2C @ ^2.0.0
+ adafruit/Adafruit Keypad @ ^1.3.0
+ adafruit/Adafruit NeoPixel @ ^1.9.0
+ adafruit/Adafruit SSD1306 @ ^2.4.6
+ adafruit/Adafruit GFX Library @ ^1.10.12
+ olikraus/U8g2_for_Adafruit_GFX @ ^1.8.0
+ adafruit/Adafruit MCP4725@^2.0.0
+
+extra_scripts = clear_storage/register_storage_target.py
+
+[env:teensy40]
+platform = teensy
+board = teensy40
+build_flags = -D USB_MIDI_SERIAL
+
+[env:teensy31]
+platform = teensy
+board = teensy31
+; build_flags = -D USB_MIDI_SERIAL -D TEENSY_OPT_FAST_LTO
+build_flags = -D USB_MIDI_SERIAL -D TEENSY_OPT_SMALLEST_CODE_LTO
+; build_flags = -D USB_MIDI_SERIAL -D TEENSY_OPT_DEBUG_LTO
+
+
diff --git a/todo.md b/Archive/todo.md
similarity index 100%
rename from todo.md
rename to Archive/todo.md
diff --git a/Battery-box/README.md b/Battery-box/README.md
new file mode 100644
index 00000000..b4a25fce
--- /dev/null
+++ b/Battery-box/README.md
@@ -0,0 +1,21 @@
+# Optional battery
+
+A lipo battery can be added to OMX-27 v3.
+
+[This battery from tinycircuits is recommended](https://tinycircuits.com/products/lithium-ion-polymer-battery-3-7v-290mah). Also available from [Mouser](https://www.mouser.com/ProductDetail/TinyCircuits/ASR00007) or [Digikey](https://www.digikey.com/en/products/detail/tinycircuits/ASR00007/7404517). [Octopart link](https://octopart.com/asr00007-tiny+circuits-84715901)
+
+The battery plug on the PCB is a JST SH connector (2 pin, 1.0 mm pitch) or you can solder the battery wires directly to the PCB. Please note the PCB markings for polarity. Looking down at the device, the red (+) terminal is on the left.
+
+A quick test gave about 4 hours of constant use on a single charge of the 290mah battery.
+
+3D printable battery box models are available below:
+
+The parts are designed to use heat-set inserts for mounting - I used M2.5 4mmx4mm inserts - and then you can just use the same screws inserted from the bottom to attach everything.
+
+
+
+[Rectangular box](omx-27_battery_box_1a.stl)
+
+
+[L-shaped box](omx-27_battery_box_1b.stl)
+
diff --git a/Battery-box/box-L.png b/Battery-box/box-L.png
new file mode 100644
index 00000000..334ac659
Binary files /dev/null and b/Battery-box/box-L.png differ
diff --git a/Battery-box/box-rect.png b/Battery-box/box-rect.png
new file mode 100644
index 00000000..f79d01ba
Binary files /dev/null and b/Battery-box/box-rect.png differ
diff --git a/Battery-box/box_inside.png b/Battery-box/box_inside.png
new file mode 100644
index 00000000..7da729eb
Binary files /dev/null and b/Battery-box/box_inside.png differ
diff --git a/Battery-box/omx-27_battery_box_1a.stl b/Battery-box/omx-27_battery_box_1a.stl
new file mode 100644
index 00000000..55cc58dd
Binary files /dev/null and b/Battery-box/omx-27_battery_box_1a.stl differ
diff --git a/Battery-box/omx-27_battery_box_1b.stl b/Battery-box/omx-27_battery_box_1b.stl
new file mode 100644
index 00000000..ef2eef99
Binary files /dev/null and b/Battery-box/omx-27_battery_box_1b.stl differ
diff --git a/OMX-27-firmware/OMX-27-firmware.ino b/OMX-27-firmware/OMX-27-firmware.ino
index e831cc0c..d2960a7a 100644
--- a/OMX-27-firmware/OMX-27-firmware.ino
+++ b/OMX-27-firmware/OMX-27-firmware.ino
@@ -1,7 +1,7 @@
// OMX-27 MIDI KEYBOARD / SEQUENCER
-// v1.13.8
-// Last update: Sept 2025
+// v1.14.0
+// Last update: Sept 2024
//
// Original concept and initial code by Steven Noreyko
// Additional code contributions:
@@ -16,11 +16,12 @@
//
#include
-#include
#include "src/consts/consts.h"
+#include "src/globals.h"
#include "src/config.h"
-#include "src/consts/colors.h"
+#include
#include "src/midi/midi.h"
+#include "src/consts/colors.h"
#include "src/ClearUI/ClearUI.h"
#include "src/modes/sequencer.h"
#include "src/midi/noteoffs.h"
@@ -37,16 +38,20 @@
#include "src/modes/omx_mode_euclidean.h"
#include "src/modes/omx_mode_chords.h"
#include "src/modes/omx_screensaver.h"
-#include "src/hardware/omx_leds.h"
#include "src/utils/music_scales.h"
+#include "src/hardware/omx_leds.h"
+#include "src/midi/MIDIClockStats.h"
// Allows code to compile with smallest code LTO
+
+#if BOARDTYPE != OMX2040
extern "C"
{
int _getpid() { return -1; }
int _kill(int pid, int sig) { return -1; }
int _write() { return -1; }
}
+#endif
// #define RAM_MONITOR
// #ifdef RAM_MONITOR
@@ -68,6 +73,8 @@ OmxScreensaver omxScreensaver;
MusicScales globalScale;
+MIDIClockStats clockstats;
+
// storage of pot values; current is in the main loop; last value is for midi output
int volatile currentValue[NUM_CC_POTS];
int lastMidiValue[NUM_CC_POTS];
@@ -81,14 +88,39 @@ uint16_t AMAX;
int V_scale;
// ENCODER
-Encoder myEncoder(12, 11); // encoder pins on hardware
-const int buttonPin = 0;
+#if BOARDTYPE == OMX2040
+ Encoder myEncoder(25, 26); // encoder pins on hardware
+ const int buttonPin = 20;
+#else
+ Encoder myEncoder(12, 11); // encoder pins on hardware
+ const int buttonPin = 0;
+#endif
int buttonState = 1;
Button encButton(buttonPin);
// long newPosition = 0;
// long oldPosition = -999;
+#if BOARDTYPE == OMX2040
+ char mfgstr[32] = "denki-oto";
+ char prodstr[32] = "omx-27-v3";
+ // MUX config
+ const int muxMapping[5] = {2,3,0,1,4}; //{A2, A3, A0, A1, A4};
+ const int mux_common_pin = 29;
+ const int mux1 = 23;
+ const int mux2 = 24;
+ const int mux3 = 22;
+ using namespace admux;
+ Mux mux(Pin(mux_common_pin, INPUT, PinType::Analog), Pinset(mux1, mux2, mux3));
+
+ // USB WebUSB object
+ // Landing Page: scheme (0: http, 1: https), url
+ // Page source can be found at https://github.com/hathach/tinyusb-webusb-page/tree/main/webusb-rgb
+ Adafruit_USBD_WebUSB usb_web;
+ WEBUSB_URL_DEF(landingPage, 1 /*https*/, "denki-oto-to-go-go.surge.sh/#/");
+
+#endif
+
// KEYPAD
// initialize an instance of custom Keypad class
unsigned long longPressInterval = 800;
@@ -96,8 +128,8 @@ unsigned long clickWindow = 200;
OMXKeypad keypad(longPressInterval, clickWindow, makeKeymap(keys), rowPins, colPins, ROWS, COLS);
// setup EEPROM/FRAM storage
-Storage *storage;
-SysEx *sysEx;
+// Storage *storage;
+// SysEx *sysEx;
#ifdef RAM_MONITOR
RamMonitor ram;
@@ -214,10 +246,13 @@ void readPotentimeters()
{
int prevValue = potSettings.analogValues[k];
int prevAnalog = potSettings.analog[k]->getValue();
-
+#if BOARDTYPE == OMX2040
+ temp = mux.read(muxMapping[k]);
+// temp = 0;
+#else
temp = analogRead(analogPins[k]);
+#endif
potSettings.analog[k]->update(temp);
-
// read from the smoother, constrain (to account for tolerances), and map it
temp = potSettings.analog[k]->getValue();
temp = constrain(temp, potMinVal, potMaxVal);
@@ -255,80 +290,11 @@ void readPotentimeters()
activeOmxMode->onPotChanged(k, prevValue, potSettings.analogValues[k], analogDelta);
}
}
- }
-}
-
-// ####### END POTENTIOMETERS #######
-void handleNoteOn(byte channel, byte note, byte velocity)
-{
- if (midiSettings.midiSoftThru)
- {
- MM::sendNoteOnHW(note, velocity, channel);
- }
- if (midiSettings.midiInToCV)
- {
- cvNoteUtil.cvNoteOn(note);
}
-
- omxScreensaver.resetCounter();
-
- activeOmxMode->inMidiNoteOn(channel, note, velocity);
-}
-
-void handleNoteOff(byte channel, byte note, byte velocity)
-{
- if (midiSettings.midiSoftThru)
- {
- MM::sendNoteOffHW(note, velocity, channel);
- }
-
- if (midiSettings.midiInToCV)
- {
- cvNoteUtil.cvNoteOff(note);
- }
-
- activeOmxMode->inMidiNoteOff(channel, note, velocity);
-}
-
-void handleControlChange(byte channel, byte control, byte value)
-{
- if (midiSettings.midiSoftThru)
- {
- MM::sendControlChangeHW(control, value, channel);
- }
- // change potbank on bank select
- if (control == 0){
- midiSettings.isBankSelect = true;
- potSettings.potbank = constrain(value, 0, NUM_CC_BANKS - 1);
- omxDisp.setDirty();
- // }else if (midiSettings.isBankSelect && control == 32){
- // midiSettings.isBankSelect = true;
- }else{
- midiSettings.isBankSelect = false;
- }
-
- activeOmxMode->inMidiControlChange(channel, control, value);
-}
-
-// #### Inbound MIDI callbacks
-void OnNoteOn(byte channel, byte note, byte velocity)
-{
- handleNoteOn(channel, note, velocity);
-}
-void OnNoteOff(byte channel, byte note, byte velocity)
-{
- handleNoteOff(channel, note, velocity);
-}
-void OnControlChange(byte channel, byte control, byte value)
-{
- handleControlChange(channel, control, value);
}
+// ####### END POTENTIOMETERS #######
-void OnSysEx(const uint8_t *data, uint16_t length, bool complete)
-{
- sysEx->processIncomingSysex(data, length);
-}
void saveHeader()
{
@@ -379,6 +345,8 @@ void saveHeader()
storage->write(EEPROM_HEADER_ADDRESS + 37, cvNoteUtil.triggerMode);
storage->write(EEPROM_HEADER_ADDRESS + 38, potSettings.potbank);
+
+ // 38 bytes
}
// returns true if the header contained initialized data
@@ -389,13 +357,13 @@ bool loadHeader(void)
char buf[64];
snprintf(buf, sizeof(buf), "EEPROM Header Version is %d\n", version);
- Serial.print(buf);
+ // Serial.print(buf);
// Uninitalized EEPROM memory is filled with 0xFF
if (version == 0xFF)
{
// EEPROM was uninitialized
- Serial.println("version was 0xFF");
+ // Serial.println("version was 0xFF");
return false;
}
@@ -403,7 +371,7 @@ bool loadHeader(void)
{
// write an adapter if we ever need to increment the EEPROM version and also save the existing patterns
// for now, return false will essentially reset the state
- Serial.println("version not matched");
+ // Serial.println("version not matched");
return false;
}
@@ -415,7 +383,7 @@ bool loadHeader(void)
uint8_t unMidiChannel = storage->read(EEPROM_HEADER_ADDRESS + 3);
sysSettings.midiChannel = unMidiChannel + 1;
- Serial.println("Loading banks");
+ // Serial.println("Loading banks");
for (int b = 0; b < NUM_CC_BANKS; b++)
{
for (int i = 0; i < NUM_CC_POTS; i++)
@@ -452,6 +420,7 @@ bool loadHeader(void)
potSettings.potbank = constrain(storage->read(EEPROM_HEADER_ADDRESS + 38), 0, NUM_CC_BANKS-1);
+ // digitalWrite(BLUELED, HIGH);
return true;
}
@@ -480,10 +449,10 @@ void savePatterns(void)
{
return;
}
- Serial.println((String)"nLocalAddress: " + nLocalAddress); // 5784
+ // Serial.println((String)"nLocalAddress: " + nLocalAddress); // 5784
#ifdef OMXMODEGRIDS
- Serial.println("Saving Grids");
+ // Serial.println("Saving Grids");
// Grids patterns
patternSize = OmxModeGrids::serializedPatternSize(isEeprom);
@@ -502,29 +471,29 @@ void savePatterns(void)
nLocalAddress += patternSize;
}
- Serial.println((String)"nLocalAddress: " + nLocalAddress); // 6008
+ // Serial.println((String)"nLocalAddress: " + nLocalAddress); // 6008
#endif
- Serial.println("Saving Euclidean");
+ // Serial.println("Saving Euclidean");
nLocalAddress = omxModeEuclid.saveToDisk(nLocalAddress, storage);
- Serial.println((String)"nLocalAddress: " + nLocalAddress); // 7433
+ // Serial.println((String)"nLocalAddress: " + nLocalAddress); // 7433
- Serial.println("Saving Chords");
+ // Serial.println("Saving Chords");
nLocalAddress = omxModeChords.saveToDisk(nLocalAddress, storage);
- Serial.println((String)"nLocalAddress: " + nLocalAddress); // 10505
+ // Serial.println((String)"nLocalAddress: " + nLocalAddress); // 10505
- Serial.println("Saving Drums");
+ // Serial.println("Saving Drums");
nLocalAddress = omxModeDrum.saveToDisk(nLocalAddress, storage);
- Serial.println((String)"nLocalAddress: " + nLocalAddress); // 11545
+ // Serial.println((String)"nLocalAddress: " + nLocalAddress); // 11545
- Serial.println("Saving MidiFX");
+ // Serial.println("Saving MidiFX");
for (uint8_t i = 0; i < NUM_MIDIFX_GROUPS; i++)
{
nLocalAddress = subModeMidiFx[i].saveToDisk(nLocalAddress, storage);
// Serial.println((String)"Saved: " + i);
// Serial.println((String)"nLocalAddress: " + nLocalAddress);
}
- Serial.println((String)"nLocalAddress: " + nLocalAddress); // 11585
+ // Serial.println((String)"nLocalAddress: " + nLocalAddress); // 11585
// Starting 11545
// MidiFX with nothing 11585
@@ -550,8 +519,8 @@ void loadPatterns(void)
int patternSize = serializedPatternSize(isEeprom);
int nLocalAddress = EEPROM_PATTERN_ADDRESS;
- Serial.print("Seq patterns - nLocalAddress: ");
- Serial.println(nLocalAddress);
+ // Serial.print("Seq patterns - nLocalAddress: ");
+ // Serial.println(nLocalAddress);
int seqPatternNum = isEeprom ? NUM_SEQ_PATTERNS_EEPROM : NUM_SEQ_PATTERNS;
@@ -574,8 +543,8 @@ void loadPatterns(void)
return;
}
- Serial.print("Grids patterns - nLocalAddress: ");
- Serial.println(nLocalAddress);
+ // Serial.print("Grids patterns - nLocalAddress: ");
+ // Serial.println(nLocalAddress);
// 332 - eeprom size
// 332 * 8 = 2656
@@ -598,34 +567,34 @@ void loadPatterns(void)
}
#endif
- Serial.print("Pattern size: ");
- Serial.print(patternSize);
+ // Serial.print("Pattern size: ");
+ // Serial.print(patternSize);
- Serial.print(" - nLocalAddress: ");
- Serial.println(nLocalAddress);
+ // Serial.print(" - nLocalAddress: ");
+ // Serial.println(nLocalAddress);
- Serial.print("Loading Euclidean - ");
+ // Serial.print("Loading Euclidean - ");
nLocalAddress = omxModeEuclid.loadFromDisk(nLocalAddress, storage);
- Serial.println((String) "nLocalAddress: " + nLocalAddress); // 5988
+ // Serial.println((String) "nLocalAddress: " + nLocalAddress); // 5988
- Serial.print("Loading Chords - ");
+ // Serial.print("Loading Chords - ");
nLocalAddress = omxModeChords.loadFromDisk(nLocalAddress, storage);
- Serial.println((String)"nLocalAddress: " + nLocalAddress); // 5988
+ // Serial.println((String)"nLocalAddress: " + nLocalAddress); // 5988
- Serial.print("Loading Drums - ");
+ // Serial.print("Loading Drums - ");
nLocalAddress = omxModeDrum.loadFromDisk(nLocalAddress, storage);
- Serial.println((String)"nLocalAddress: " + nLocalAddress); // 5988
+ // Serial.println((String)"nLocalAddress: " + nLocalAddress); // 5988
// Serial.println((String)"nLocalAddress: " + nLocalAddress); // 5968
- Serial.print("Loading MidiFX - ");
+ // Serial.print("Loading MidiFX - ");
for (uint8_t i = 0; i < NUM_MIDIFX_GROUPS; i++)
{
nLocalAddress = subModeMidiFx[i].loadFromDisk(nLocalAddress, storage);
// Serial.println((String)"Loaded: " + i);
// Serial.println((String)"nLocalAddress: " + nLocalAddress);
}
- Serial.println((String) "nLocalAddress: " + nLocalAddress); // 5988
+ // Serial.println((String) "nLocalAddress: " + nLocalAddress); // 5988
// with 8 note chords, 10929
@@ -648,7 +617,7 @@ void loadPatterns(void)
// currently saves everything ( mode + patterns )
void saveToStorage(void)
{
- Serial.println("Saving to Storage...");
+ // Serial.println("Saving to Storage...");
saveHeader();
savePatterns();
}
@@ -656,15 +625,16 @@ void saveToStorage(void)
// currently loads everything ( mode + patterns )
bool loadFromStorage(void)
{
- // This load can happen soon after Serial.begin - enable this 'wait for Serial' if you need to Serial.print during loading
+ // This load can happen soon after Serial.begin
+ // - enable this 'wait for Serial' if you need to Serial.print during loading
// while( !Serial );
- Serial.println("Read the header");
+ // Serial.println("Read the header");
bool bContainedData = loadHeader();
if (bContainedData)
{
- Serial.println("Loading patterns");
+ // Serial.println("Loading patterns");
loadPatterns();
changeOmxMode(sysSettings.omxMode);
@@ -673,7 +643,7 @@ bool loadFromStorage(void)
return true;
}
- Serial.println("-- Failed to load --");
+ // Serial.println("-- Failed to load --");
omxDisp.isDirty();
omxLeds.isDirty();
@@ -710,7 +680,7 @@ void loop()
omxUtil.advanceSteps(passed);
}
- // DISPLAY SETUP
+ // DISPLAY SETUP -- why is this display. instead of omxDisp. ??
display.clearDisplay();
// ############### SLEEP MODE ###############
@@ -742,6 +712,7 @@ void loop()
// ############### ENCODER ###############
//
auto u = myEncoder.update();
+// Serial.println("Encoder update");
if (u.active())
{
auto amt = u.accel(1); // where 5 is the acceleration factor if you want it, 0 if you don't)
@@ -834,6 +805,7 @@ void loop()
//
while (keypad.available())
{
+// Serial.println("keypad");
auto e = keypad.next();
int thisKey = e.key();
bool keyConsumed = false;
@@ -905,7 +877,6 @@ void loop()
// DISPLAY at end of loop
omxDisp.showDisplay();
-
omxLeds.showLeds();
while (MM::usbMidiRead())
@@ -914,9 +885,11 @@ void loop()
}
while (MM::midiRead())
{
- // ignore incoming messages
+ // incoming messages - see handlers
}
+ // Serial.println(clockstats.getBPM());
+
// Micros elapsed = micros() - timeStart;
// if ((timeStart - reporttime) > 2000)
// {
@@ -939,77 +912,122 @@ void loop()
} // ######## END MAIN LOOP ########
+
+
// ####### SETUP #######
void setup()
{
- Serial.begin(115200);
- // while( !Serial );
-#if T4
- Serial.println("Teensy 4.0");
- // Serial.println("DAC Start!");
+
+#if BOARDTYPE == TEENSY4
+// Serial.println("Teensy 4.0");
+// Serial.println("DAC Start!");
dac.begin(DAC_ADDR);
+
+#elif BOARDTYPE == OMX2040
+// Serial.println("RP2040");
+ TinyUSBDevice.setManufacturerDescriptor(mfgstr);
+ TinyUSBDevice.setProductDescriptor(prodstr);
+
+ pinMode(REDLED, OUTPUT); // RED LED
+ pinMode(BLUELED, OUTPUT); // BLUE LED
+ digitalWrite(REDLED, LOW); // digitalWrite(REDLED, LOW);
+ digitalWrite(BLUELED, HIGH);
+
+ pinMode(FIVEVEN, OUTPUT); // 5v enable Pin
+ digitalWrite(FIVEVEN, HIGH); // Turn 5v enable ON
+
+ pinMode(TXLED, OUTPUT); // TX
+ pinMode(RXLED, INPUT); // RX
+
+ digitalWrite(TXLED, LOW);
+ digitalWrite(RXLED, LOW);
+ Wire1.setSDA(I2C_SDA); // i2c1 SDA
+ Wire1.setSCL(I2C_SCL); // i2c1 SCL
+
+// Serial1.setRX(RXLED);
+// Serial1.setTX(TXLED);
+
+ dac.begin(DAC_ADDR, &Wire1);
+
+ // Initialize WebUSB for connection notification, etc
+ usb_web.setLandingPage(&landingPage);
+ usb_web.begin();
+
#else
- Serial.println("Teensy 3.2");
+// Serial.println("Teensy 3.2");
#endif
- // Init Display
- omxDisp.setup();
- // Startup screen
- omxDisp.drawStartupScreen();
+ // HW MIDI
+ MM::begin();
- // Storage
+ // CV GATE pin
+ pinMode(CVGATE_PIN, OUTPUT);
+ // ENCODER BUTTON pin
+ pinMode(buttonPin, INPUT_PULLUP);
+
+ // Storage - FIX?
storage = Storage::initStorage();
sysEx = new SysEx(storage, &sysSettings);
+ // Serial.println( "initStorage" );
#ifdef RAM_MONITOR
ram.initialize();
#endif
- // incoming usbMIDI callbacks
- usbMIDI.setHandleNoteOff(OnNoteOff);
- usbMIDI.setHandleNoteOn(OnNoteOn);
- usbMIDI.setHandleControlChange(OnControlChange);
- usbMIDI.setHandleSystemExclusive(OnSysEx);
-
// clksTimer = 0; // TODO - didn't see this used anywhere
omxScreensaver.resetCounter();
// ssstep = 0;
lastProcessTime = micros();
- omxUtil.resetClocks();
+ omxUtil.restartClocks();
omxUtil.subModeClearStorage.setStoragePtr(storage);
- // HW MIDI
- MM::begin();
- randomSeed(analogRead(13));
- srand(analogRead(13));
+// #if BOARDTYPE == OMX2040
+ // while (!TinyUSBDevice.mounted()){
+ // delay(100);
+ // }
+// #endif
+
+ // Serial
+ Serial.begin(115200);
+ delay(100);
+
// SET ANALOG READ resolution to teensy's 13 usable bits
-#if T4
+#if BOARDTYPE == TEENSY4
+ randomSeed(analogRead(13));
+ srand(analogRead(13));
analogReadResolution(10); // Teensy 4 = 10 bits
+#elif BOARDTYPE == OMX2040
+// randomSeed(analogRead(29));
+// srand(analogRead(29));
+ analogReadResolution(10); // MUX = 10 bits
#else
+ randomSeed(analogRead(13));
+ srand(analogRead(13));
analogReadResolution(13); // Teensy 3.x = 13 bits
#endif
- // CV GATE pin
- pinMode(CVGATE_PIN, OUTPUT);
- // ENCODER BUTTON pin
- pinMode(buttonPin, INPUT_PULLUP);
// initialize ANALOG INPUTS and ResponsiveAnalogRead
for (int i = 0; i < potCount; i++)
{
- // potSettings.analog[i] = new ResponsiveAnalogRead(0, true, .001);
- // potSettings.analog[i]->setAnalogResolution(1 << 13);
+// potSettings.analog[i] = new ResponsiveAnalogRead(0, true, .001);
+// potSettings.analog[i]->setAnalogResolution(1 << 13);
+
+#if BOARDTYPE == TEENSY4
pinMode(analogPins[i], INPUT);
potSettings.analog[i] = new ResponsiveAnalogRead(analogPins[i], true, .001);
+// potSettings.analog[i]->setAnalogResolution(10);
+// potSettings.analog[i]->setActivityThreshold(8);
+#elif BOARDTYPE == OMX2040
+ potSettings.analog[i] = new ResponsiveAnalogRead(mux_common_pin, true, .001);
-#if T4
- // potSettings.analog[i]->setAnalogResolution(10);
- // potSettings.analog[i]->setActivityThreshold(8);
#else
+ pinMode(analogPins[i], INPUT);
+ potSettings.analog[i] = new ResponsiveAnalogRead(analogPins[i], true, .001);
potSettings.analog[i]->setAnalogResolution(1 << 13);
potSettings.analog[i]->setActivityThreshold(32);
#endif
@@ -1023,7 +1041,9 @@ void setup()
AMAX = pow(2, RES);
V_scale = 64; // pow(2,(RES-7)); 4095 max
-#if T4
+#if BOARDTYPE == TEENSY4
+ dac.setVoltage(0, false);
+#elif BOARDTYPE == OMX2040
dac.setVoltage(0, false);
#else
analogWriteResolution(RES); // set resolution for DAC
@@ -1034,21 +1054,40 @@ void setup()
omxModeMidi.SetScale(&globalScale);
omxModeDrum.SetScale(&globalScale);
omxModeSeq.SetScale(&globalScale);
+
#ifdef OMXMODEGRIDS
omxModeGrids.SetScale(&globalScale);
#endif
omxModeEuclid.SetScale(&globalScale);
omxModeChords.SetScale(&globalScale);
- // Load from EEPROM
+ // Keypad
+ // customKeypad.begin();
+ keypad.begin();
+ // Serial.println( "Init keypad" );
+
+ // Init Display
+ omxDisp.setup();
+
+ // Startup screen
+ omxDisp.drawStartupScreen();
+
+ // LEDs
+ omxLeds.initSetup();
+ // Serial.println( "Init LEDs" );
+
+
+ // Load settings from EEPROM
+ // bool bLoaded = false; // loadFromStorage();
+ // Serial.println( "Load from EEPROM" );
bool bLoaded = loadFromStorage();
+
if (!bLoaded)
{
- Serial.println( "Init load fail. Reinitializing" );
+ // Serial.println( "Init load fail. Reinitializing" );
// Failed to load due to initialized EEPROM or version mismatch
// defaults
- // sysSettings.omxMode = DEFAULT_MODE;
sequencer.playingPattern = 0;
sysSettings.playingPattern = 0;
sysSettings.midiChannel = 1;
@@ -1065,17 +1104,12 @@ void setup()
saveToStorage();
}
- // Keypad
- // customKeypad.begin();
- keypad.begin();
-
- // LEDs
- omxLeds.initSetup();
-
#ifdef RAM_MONITOR
reporttime = millis();
#endif
+
+
}
// ####### END SETUP #######
diff --git a/OMX-27-firmware/src/ClearUI/ClearUI_Display.cpp b/OMX-27-firmware/src/ClearUI/ClearUI_Display.cpp
index a796664c..64a3b6ec 100644
--- a/OMX-27-firmware/src/ClearUI/ClearUI_Display.cpp
+++ b/OMX-27-firmware/src/ClearUI/ClearUI_Display.cpp
@@ -36,7 +36,11 @@
#define CLKDURING 1000000
#define CLKAFTER 400000
+#if BOARDTYPE == OMX2040
+Adafruit_SSD1306 display = Adafruit_SSD1306(DISPLAY_WIDTH, DISPLAY_HEIGHT, &Wire1, OLED_RST, CLKDURING, CLKAFTER);
+#else
Adafruit_SSD1306 display = Adafruit_SSD1306(DISPLAY_WIDTH, DISPLAY_HEIGHT, &Wire, OLED_RST, CLKDURING, CLKAFTER);
+#endif
void initializeDisplay()
{
@@ -167,38 +171,38 @@ namespace
int16_t saverPhase;
const unsigned char wipePattern[] = { // 24 x 32
- B00000010, B10101011, B11111111,
- B00000010, B10101011, B11111111,
- B00000010, B10101011, B11111111,
- B00000101, B01010111, B11111110,
- B00000101, B01010111, B11111110,
- B00000101, B01010111, B11111110,
- B00000101, B01010111, B11111110,
- B00000101, B01010111, B11111110,
- B00000101, B01010111, B11111110,
- B00001010, B10101111, B11111100,
- B00001010, B10101111, B11111100,
- B00001010, B10101111, B11111100,
- B00001010, B10101111, B11111100,
- B00001010, B10101111, B11111100,
- B00010101, B01011111, B11111000,
- B00010101, B01011111, B11111000,
- B00010101, B01011111, B11111000,
- B00010101, B01011111, B11111000,
- B00010101, B01011111, B11111000,
- B00101010, B10111111, B11110000,
- B00101010, B10111111, B11110000,
- B00101010, B10111111, B11110000,
- B00101010, B10111111, B11110000,
- B00101010, B10111111, B11110000,
- B00101010, B10111111, B11110000,
- B01010101, B01111111, B11100000,
- B01010101, B01111111, B11100000,
- B01010101, B01111111, B11100000,
- B01010101, B01111111, B11100000,
- B01010101, B01111111, B11100000,
- B10101010, B11111111, B11000000,
- B10101010, B11111111, B11000000,
+ 0b00000010, 0b10101011, 0b11111111,
+ 0b00000010, 0b10101011, 0b11111111,
+ 0b00000010, 0b10101011, 0b11111111,
+ 0b00000101, 0b01010111, 0b11111110,
+ 0b00000101, 0b01010111, 0b11111110,
+ 0b00000101, 0b01010111, 0b11111110,
+ 0b00000101, 0b01010111, 0b11111110,
+ 0b00000101, 0b01010111, 0b11111110,
+ 0b00000101, 0b01010111, 0b11111110,
+ 0b00001010, 0b10101111, 0b11111100,
+ 0b00001010, 0b10101111, 0b11111100,
+ 0b00001010, 0b10101111, 0b11111100,
+ 0b00001010, 0b10101111, 0b11111100,
+ 0b00001010, 0b10101111, 0b11111100,
+ 0b00010101, 0b01011111, 0b11111000,
+ 0b00010101, 0b01011111, 0b11111000,
+ 0b00010101, 0b01011111, 0b11111000,
+ 0b00010101, 0b01011111, 0b11111000,
+ 0b00010101, 0b01011111, 0b11111000,
+ 0b00101010, 0b10111111, 0b11110000,
+ 0b00101010, 0b10111111, 0b11110000,
+ 0b00101010, 0b10111111, 0b11110000,
+ 0b00101010, 0b10111111, 0b11110000,
+ 0b00101010, 0b10111111, 0b11110000,
+ 0b00101010, 0b10111111, 0b11110000,
+ 0b01010101, 0b01111111, 0b11100000,
+ 0b01010101, 0b01111111, 0b11100000,
+ 0b01010101, 0b01111111, 0b11100000,
+ 0b01010101, 0b01111111, 0b11100000,
+ 0b01010101, 0b01111111, 0b11100000,
+ 0b10101010, 0b11111111, 0b11000000,
+ 0b10101010, 0b11111111, 0b11000000,
};
}
diff --git a/OMX-27-firmware/src/config.cpp b/OMX-27-firmware/src/config.cpp
index 96ae5bec..ea50b3db 100644
--- a/OMX-27-firmware/src/config.cpp
+++ b/OMX-27-firmware/src/config.cpp
@@ -2,7 +2,7 @@
#include "consts/consts.h"
const OMXMode DEFAULT_MODE = MODE_MIDI;
-const uint8_t EEPROM_VERSION = 36;
+const uint8_t EEPROM_VERSION = 38;
// v30 - adds storage to header for velocity
// v31 - adds storage for drums
@@ -23,25 +23,32 @@ const int CC_AUX = 25; // Mother mode - AUX key
const int CC_OM1 = 26; // Mother mode - enc switch
const int CC_OM2 = 28; // Mother mode - enc turn
-const int LED_BRIGHTNESS = 50;
+const int LED_BRIGHTNESS = 90;
// DONT CHANGE ANYTHING BELOW HERE
-
-const int LED_PIN = 14;
const int LED_COUNT = 27;
+#if BOARDTYPE == OMX2040
+ const int LED_PIN = 19;
+#else
+ const int LED_PIN = 14;
+#endif
+
#if DEV
-const int analogPins[] = {23, 22, 21, 20, 16}; // DEV/beta boards
-const byte DAC_ADDR = 0x62;
+ const int analogPins[] = {23, 22, 21, 20, 16}; // DEV/beta boards
+ const byte DAC_ADDR = 0x62;
#elif MIDIONLY
-const int analogPins[] = {23, 22, 21, 20, 16}; // on MIDI only boards - {23,A10,21,20,16} on Bodged MIDI boards
-const byte DAC_ADDR = 0x60;
-#elif T4
-const int analogPins[] = {23, 22, 21, 20, 16}; // on 2.0
-const byte DAC_ADDR = 0x60;
+ const int analogPins[] = {23, 22, 21, 20, 16}; // on MIDI only boards - {23,A10,21,20,16} on Bodged MIDI boards
+ const byte DAC_ADDR = 0x60;
+#elif BOARDTYPE == TEENSY4
+ const int analogPins[] = {23, 22, 21, 20, 16}; // on 2.0
+ const byte DAC_ADDR = 0x60;
+#elif BOARDTYPE == OMX2040
+ const int analogPins[] = {2, 3 ,0, 1, 4}; // mux pin numbers
+ const byte DAC_ADDR = 0x60;
#else
-const int analogPins[] = {34, 22, 21, 20, 16}; // on 1.0
-const byte DAC_ADDR = 0x60;
+ const int analogPins[] = {34, 22, 21, 20, 16}; // on 1.0
+ const byte DAC_ADDR = 0x60;
#endif
const int potCount = NUM_CC_POTS;
@@ -54,10 +61,12 @@ int pots[NUM_CC_BANKS][NUM_CC_POTS] = {
{91, 93, 103, 104, 7}}; // the MIDI CC (continuous controller) for each analog input
int potMinVal = 0;
-#if T4
-int potMaxVal = 1019; // T4 = 1019 // T3.2 = 8191;
+#if BOARDTYPE == TEENSY4
+ int potMaxVal = 1019; // T4 = 1019 // T3.2 = 8190;
+#elif BOARDTYPE == OMX2040
+ int potMaxVal = 1018;
#else
-int potMaxVal = 8191; // T4 = 1019 // T3.2 = 8191;
+ int potMaxVal = 8191; // T4 = 1019 // T3.2 = 8191;
#endif
const int gridh = 32;
@@ -99,8 +108,13 @@ char keys[ROWS][COLS] = {
{11, 12, 13, 14, 15, 24},
{16, 17, 18, 19, 20, 25},
{22, 23, 21}};
-byte rowPins[ROWS] = {6, 4, 3, 5, 2}; // row pins for key switches
-byte colPins[COLS] = {7, 8, 10, 9, 15, 17}; // column pins for key switches
+#if BOARDTYPE == OMX2040
+ byte rowPins[ROWS] = {28, 14, 13, 12, 6}; // row pins for key switches
+ byte colPins[COLS] = {10, 9, 4, 5, 8, 11}; // column pins for key switches
+#else
+ byte rowPins[ROWS] = {6, 4, 3, 5, 2}; // row pins for key switches
+ byte colPins[COLS] = {7, 8, 10, 9, 15, 17}; // column pins for key switches
+#endif
// KEYBOARD MIDI NOTE LAYOUT
const int notes[] = {0,
@@ -114,10 +128,7 @@ const int steps[] = {0,
const int midiKeyMap[] = {12, 1, 13, 2, 14, 15, 3, 16, 4, 17, 5, 18, 19, 6, 20, 7, 21, 22, 8, 23, 9, 24, 10, 25, 26};
Adafruit_MCP4725 dac;
-SysSettings sysSettings;
-PotSettings potSettings;
MidiConfig midiSettings;
-MidiMacroConfig midiMacroConfig;
EncoderConfig encoderConfig;
ClockConfig clockConfig;
SequencerConfig seqConfig;
diff --git a/OMX-27-firmware/src/config.h b/OMX-27-firmware/src/config.h
index d5179673..6ce20d53 100644
--- a/OMX-27-firmware/src/config.h
+++ b/OMX-27-firmware/src/config.h
@@ -11,20 +11,20 @@
#include
#include
#include "consts/colors.h"
+#include
// #include
/* * firmware metadata */
-// OMX_VERSION = 1.13.8
+// OMX_VERSION = 1.14.0
const int MAJOR_VERSION = 1;
-const int MINOR_VERSION = 13;
-const int POINT_VERSION = 8;
+const int MINOR_VERSION = 14;
+const int POINT_VERSION = 0;
// 1.13.2 - Adds CV Trigger modes for legato and regtrig
// 1.13.3 - Bugfix for CV Trigger modes
-// 1.13.5 - Bugfix for grids T4 pots
-// 1.13.6 - start/stop midi fixes in grids, sysex tweaks for pot banks
// 1.13.8 - option to send midi all the time or not
+// 1.14.0 - finish RP2040 port
const int DEVICE_ID = 2;
@@ -102,7 +102,7 @@ extern const int LED_BRIGHTNESS;
extern const int LED_PIN;
extern const int LED_COUNT;
-// POTS/ANALOG INPUTS - teensy pins for analog inputs
+// POTS/ANALOG INPUTS - pins for analog inputs
extern const int analogPins[];
#define NUM_CC_BANKS 5
@@ -123,7 +123,7 @@ struct SysSettings
unsigned long timeElasped;
};
-extern SysSettings sysSettings;
+
extern const int potCount;
@@ -141,7 +141,6 @@ struct PotSettings
int potNum = 0;
};
// Put in global struct to share across classes
-extern PotSettings potSettings;
extern int potMinVal;
extern int potMaxVal;
@@ -193,7 +192,6 @@ struct MidiMacroConfig
int midiMacroChan = 10;
};
-extern MidiMacroConfig midiMacroConfig;
// extern bool m8mutesolo[];
@@ -298,9 +296,9 @@ struct ColorConfig
uint32_t mfxQuickEdit = RED;
- uint32_t mfxNone = LEDOFF;
- uint32_t mfxChance = MEDRED;
- uint32_t mfxTranspose = PURPLE;
+ uint32_t mfxNone = LEDOFF;
+ uint32_t mfxChance = MEDRED;
+ uint32_t mfxTranspose = PURPLE;
uint32_t mfxRandomizer = RED;
uint32_t mfxSelector = ORANGE;
uint32_t mfxChord = CYAN;
diff --git a/OMX-27-firmware/src/consts/consts.h b/OMX-27-firmware/src/consts/consts.h
index b53b3c75..bc478fb6 100644
--- a/OMX-27-firmware/src/consts/consts.h
+++ b/OMX-27-firmware/src/consts/consts.h
@@ -4,34 +4,72 @@
// HW_VERSIONS
+#define PICO 1
+#define OMX2040 2
+#define TEENSY32 3
+#define TEENSY4 4
+
+#ifndef BOARDTYPE
+
// AUTOMATICALLY GET BOARD TYPE - DO NOT MODIFY
#ifdef ARDUINO_TEENSY40
-#define T4 1
+// #define T4 1
+ #define BOARDTYPE TEENSY4
+#elif ARDUINO_TEENSY32
+ #define BOARDTYPE TEENSY32
#else
-#define T4 0
+// #define T4 0
+ #define BOARDTYPE OMX2040
+#endif
+
#endif
+// #ifndef BOARDTYPE
+// #define BOARDTYPE OMX2040
+// #endif
+
+
#define DEV 0
#define MIDIONLY 0
+
+#if BOARDTYPE == OMX2040
+ // I2C pin defs
+ const uint8_t I2C_SDA = 2;
+ const uint8_t I2C_SCL = 3;
+
+ // pin defs
+ const uint8_t TXLED = 0;
+ const uint8_t RXLED = 1;
+ const int REDLED = 16; // RED LED
+ const int BLUELED = 18; // BLUE LED
+ const int NEOPIXPIN = 19;
+ const int FIVEVEN = 17;
+
+#endif
+
// Comment out defines to disable modes if needed for debug build
#define OMXMODEGRIDS
// HARDWARE Pin for CVGATE_PIN = 13 on beta1 boards, 22 on bodge/midi, 23 on 1.0
#if DEV
-const int CVGATE_PIN = 13;
-#elif T4
-const int CVGATE_PIN = 13;
+ const int CVGATE_PIN = 13;
+#elif BOARDTYPE == TEENSY4
+ const int CVGATE_PIN = 13;
#elif MIDIONLY
-const int CVGATE_PIN = 22; // 13 on beta1 boards, A10 (broken) on test/midi, 23 on 1.0
+ const int CVGATE_PIN = 22; // 13 on beta1 boards, A10 (broken) on test/midi, 23 on 1.0
+#elif BOARDTYPE == OMX2040
+ const int CVGATE_PIN = 27;
#else
-const int CVGATE_PIN = 23; // 13 on beta1 boards, 22 on test, 23 on 1.0
+ const int CVGATE_PIN = 23; // 13 on beta1 boards, 22 on test, 23 on 1.0
#endif
-#if T4
+#if BOARDTYPE == TEENSY4
// const int CVPITCH_PIN = A14;
+#elif BOARDTYPE == OMX2040
+
#else
-const int CVPITCH_PIN = A14;
+ const int CVPITCH_PIN = A14;
#endif
const int loSkip = 0;
diff --git a/OMX-27-firmware/src/globals.cpp b/OMX-27-firmware/src/globals.cpp
new file mode 100644
index 00000000..6b1e572a
--- /dev/null
+++ b/OMX-27-firmware/src/globals.cpp
@@ -0,0 +1,11 @@
+
+#include "globals.h"
+
+SysSettings sysSettings;
+PotSettings potSettings;
+MidiMacroConfig midiMacroConfig;
+
+// setup EEPROM/FRAM storage
+Storage *storage;
+SysEx *sysEx;
+
diff --git a/OMX-27-firmware/src/globals.h b/OMX-27-firmware/src/globals.h
new file mode 100644
index 00000000..440f5867
--- /dev/null
+++ b/OMX-27-firmware/src/globals.h
@@ -0,0 +1,14 @@
+#include "config.h"
+#include "hardware/storage.h"
+#include "midi/sysex.h"
+#include "midi/MIDIClockStats.h"
+
+extern SysSettings sysSettings;
+extern PotSettings potSettings;
+extern MidiMacroConfig midiMacroConfig;
+extern MIDIClockStats clockstats;
+
+// setup EEPROM/FRAM storage
+extern Storage *storage;
+extern SysEx *sysEx;
+
diff --git a/OMX-27-firmware/src/hardware/omx_disp.cpp b/OMX-27-firmware/src/hardware/omx_disp.cpp
index 36c6fbfb..bdb76c73 100644
--- a/OMX-27-firmware/src/hardware/omx_disp.cpp
+++ b/OMX-27-firmware/src/hardware/omx_disp.cpp
@@ -3,6 +3,7 @@
#include "omx_disp.h"
#include "../consts/consts.h"
#include "../ClearUI/ClearUI.h"
+#include "../globals.h"
U8G2_FOR_ADAFRUIT_GFX u8g2_display;
@@ -256,7 +257,7 @@ bool OmxDisp::validateLegendIndex(uint8_t index)
{
if(index >= 4)
{
- Serial.println("ERROR: Param index out of range!");
+// Serial.println("ERROR: Param index out of range!");
return false;
}
return true;
diff --git a/OMX-27-firmware/src/hardware/omx_disp.h b/OMX-27-firmware/src/hardware/omx_disp.h
index 1970d9ec..76b9b722 100644
--- a/OMX-27-firmware/src/hardware/omx_disp.h
+++ b/OMX-27-firmware/src/hardware/omx_disp.h
@@ -1,5 +1,6 @@
#pragma once
#include "../config.h"
+#include
// MESSAGE DISPLAY
const int MESSAGE_TIMEOUT_US = 500000;
@@ -8,7 +9,7 @@ class OmxDisp
{
public:
// Should make into function
-
+
const char *legends[4] = {"", "", "", ""};
int legendVals[4] = {0, 0, 0, 0};
int dispPage = 0;
diff --git a/OMX-27-firmware/src/hardware/omx_leds.cpp b/OMX-27-firmware/src/hardware/omx_leds.cpp
index 35b9ff6c..65805e51 100644
--- a/OMX-27-firmware/src/hardware/omx_leds.cpp
+++ b/OMX-27-firmware/src/hardware/omx_leds.cpp
@@ -1,6 +1,7 @@
#include "omx_leds.h"
#include "../consts/consts.h"
#include "../consts/colors.h"
+#include "../globals.h"
Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800);
diff --git a/OMX-27-firmware/src/hardware/omx_leds.h b/OMX-27-firmware/src/hardware/omx_leds.h
index f1af5ae4..b9b01c65 100644
--- a/OMX-27-firmware/src/hardware/omx_leds.h
+++ b/OMX-27-firmware/src/hardware/omx_leds.h
@@ -4,6 +4,7 @@
#include "../utils/music_scales.h"
#include
+#include
// Declare NeoPixel strip object
extern Adafruit_NeoPixel strip;
@@ -13,7 +14,7 @@ class OmxLeds
public:
static const int octDnColor = ORANGE;
static const int octUpColor = RBLUE;
-
+
// OmxLeds() : strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800){};
OmxLeds(){};
@@ -69,7 +70,7 @@ class OmxLeds
uint8_t blinkPatPos[10];
const uint8_t blinkPatternDelay_ = 2;
-
+
};
extern OmxLeds omxLeds;
diff --git a/OMX-27-firmware/src/hardware/storage.cpp b/OMX-27-firmware/src/hardware/storage.cpp
index 2f443311..075623d3 100644
--- a/OMX-27-firmware/src/hardware/storage.cpp
+++ b/OMX-27-firmware/src/hardware/storage.cpp
@@ -1,6 +1,7 @@
#include
#include
#include
+#include
#include "storage.h"
@@ -10,10 +11,17 @@ Storage *Storage::initStorage()
{
Adafruit_FRAM_I2C fram = Adafruit_FRAM_I2C();
// check if FRAM chip can be initialised
+#if BOARDTYPE == OMX2040
+ if (fram.begin(0x50, &Wire1))
+ {
+ return new FRAMStorage(fram);
+ }
+#else
if (fram.begin())
{
return new FRAMStorage(fram);
}
+#endif
// fall back to EEPROM
return new EEPROMStorage();
}
diff --git a/OMX-27-firmware/src/hardware/storage.h b/OMX-27-firmware/src/hardware/storage.h
index 8db2f3b2..8fd2dd30 100644
--- a/OMX-27-firmware/src/hardware/storage.h
+++ b/OMX-27-firmware/src/hardware/storage.h
@@ -8,6 +8,12 @@ enum StorageType
FRAM_MEMORY = 1
};
+// EEPROM available
+// Teensy 3.2 2048 bytes
+// Teensy 4.0 1080 bytes
+// FRAM = 32 KBytes
+
+
// abstract storage class
class Storage
{
diff --git a/OMX-27-firmware/src/midi/MIDIClockStats.h b/OMX-27-firmware/src/midi/MIDIClockStats.h
new file mode 100644
index 00000000..9fe3ac72
--- /dev/null
+++ b/OMX-27-firmware/src/midi/MIDIClockStats.h
@@ -0,0 +1,114 @@
+// MIDIClockStats.h
+#pragma once
+
+using Micros = unsigned long;
+
+class MIDIClockStats
+{
+private:
+ static const size_t WINDOW_SIZE = 48; // Store 2 quarter notes worth of timing data (24 PPQ * 2)
+ static const size_t PPQ_IN = 24; // Incoming MIDI Clock Pulses Per Quarter Note
+ static constexpr float MICROS_PER_MINUTE = 60.0f * 1000000.0f;
+
+ // Simple circular buffer for last N intervals
+ Micros intervals[WINDOW_SIZE];
+ size_t writeIndex;
+ size_t count;
+
+ // Last timestamp for interval calculation
+ Micros lastTimestamp;
+
+ // Running statistics
+ float currentBPM;
+ bool isRunning;
+
+ // Debug
+ Micros lastInterval;
+
+public:
+ MIDIClockStats() : writeIndex(0),
+ count(0),
+ lastTimestamp(0),
+ currentBPM(120.0f),
+ isRunning(false),
+ lastInterval(0)
+ {
+ for (size_t i = 0; i < WINDOW_SIZE; i++)
+ {
+ intervals[i] = 0;
+ }
+ }
+
+ void clockPulse(Micros currentTime)
+ {
+ if (!isRunning)
+ return;
+
+ // Calculate interval if we have a previous timestamp
+ if (lastTimestamp > 0)
+ {
+ Micros interval = currentTime - lastTimestamp;
+
+ // Store in circular buffer
+ intervals[writeIndex] = interval;
+ writeIndex = (writeIndex + 1) % WINDOW_SIZE;
+ if (count < WINDOW_SIZE)
+ count++;
+
+ // Calculate average interval
+ Micros totalInterval = 0;
+ size_t samplesToUse = count;
+
+ for (size_t i = 0; i < samplesToUse; i++)
+ {
+ totalInterval += intervals[i];
+ }
+
+ float avgInterval = float(totalInterval) / samplesToUse;
+ lastInterval = interval; // Store for debugging
+
+ // Calculate BPM
+ if (avgInterval > 0)
+ {
+ float newBPM = MICROS_PER_MINUTE / (avgInterval * PPQ_IN);
+ // Simple low-pass filter
+ // More aggressive smoothing to handle jittery sources like DAWs
+ currentBPM = currentBPM * 0.95f + newBPM * 0.05f;
+ }
+ }
+
+ lastTimestamp = currentTime;
+ }
+
+ void start()
+ {
+ reset();
+ isRunning = true;
+ }
+
+ void stop()
+ {
+ isRunning = false;
+ }
+
+ void reset()
+ {
+ writeIndex = 0;
+ count = 0;
+ lastTimestamp = 0;
+ // Don't reset currentBPM to allow for smooth restarts
+
+ for (size_t i = 0; i < WINDOW_SIZE; i++)
+ {
+ intervals[i] = 0;
+ }
+ }
+
+ // Getters
+ float getBPM() const { return currentBPM; }
+ bool isClockRunning() const { return isRunning; }
+
+ // Debug getters
+ Micros getLastInterval() const { return lastInterval; }
+ size_t getSampleCount() const { return count; }
+};
\ No newline at end of file
diff --git a/OMX-27-firmware/src/midi/midi.cpp b/OMX-27-firmware/src/midi/midi.cpp
index 841dd483..c2aaf410 100644
--- a/OMX-27-firmware/src/midi/midi.cpp
+++ b/OMX-27-firmware/src/midi/midi.cpp
@@ -1,26 +1,214 @@
-#include "./midi.h"
#include
+#include "../globals.h"
+#include "./midi.h"
+#include "../consts/consts.h"
+#include "../config.h"
+#include "../utils/omx_util.h"
+#include "../hardware/omx_disp.h"
+#include "../utils/cvNote_util.h"
+#include "../modes/omx_screensaver.h"
+#include "../modes/omx_mode_interface.h"
+#include "../modes/sequencer.h"
+#include "sysex.h"
+
+extern OmxModeInterface *activeOmxMode;
+extern OmxScreensaver omxScreensaver;
+extern SequencerState sequencer;
+
namespace
{
+
+#if BOARDTYPE == OMX2040
+ Adafruit_USBD_MIDI usb_midi;
+ MIDI_CREATE_INSTANCE(Adafruit_USBD_MIDI, usb_midi, usbMIDI); // USBMIDI is USB MIDI
+ MIDI_CREATE_INSTANCE(HardwareSerial, Serial1, HWMIDI); // HWMIDI is Hardware MIDI
+
+#else
using SerialMIDI = midi::SerialMIDI;
using MidiInterface = midi::MidiInterface;
-
SerialMIDI theSerialInstance(Serial1);
MidiInterface HWMIDI(theSerialInstance);
+
+#endif
}
namespace MM
-{
+{
void begin()
{
- HWMIDI.begin();
+ #if BOARDTYPE == OMX2040
+ HWMIDI.begin(MIDI_CHANNEL_OMNI);
+ usbMIDI.begin(MIDI_CHANNEL_OMNI);
+
+ HWMIDI.turnThruOff();
+ usbMIDI.turnThruOff();
+
+ // handlers / callbacks
+ usbMIDI.setHandleNoteOn(handleNoteOn);
+ usbMIDI.setHandleNoteOff(handleNoteOff);
+ usbMIDI.setHandleClock(handleClock);
+ usbMIDI.setHandleStart(handleStart);
+ usbMIDI.setHandleStop(handleStop);
+ usbMIDI.setHandleContinue(handleContinue);
+ usbMIDI.setHandleControlChange(handleControlChange);
+ usbMIDI.setHandleSystemExclusive(OnSysEx);
+
+ HWMIDI.setHandleNoteOn(handleNoteOn);
+ HWMIDI.setHandleNoteOff(handleNoteOff);
+ HWMIDI.setHandleClock(handleClock);
+ HWMIDI.setHandleStart(handleStart);
+ HWMIDI.setHandleStop(handleStop);
+ HWMIDI.setHandleContinue(handleContinue);
+ HWMIDI.setHandleControlChange(handleControlChange);
+ HWMIDI.setHandleSystemExclusive(OnSysExHW);
+ #else
+ HWMIDI.begin();
+ #endif
+ }
+ // #### Inbound MIDI callbacks
+
+ // void onControlChange(byte channel, byte number, byte value){
+ // // if bank select MSB (0)- set flag
+ // // if flag, then look for next CC - LSB (32),
+ // // then do bank change and reset flag
+ // // or if not 32, reset flag
+ // }
+
+ void handleNoteOn(byte channel, byte note, byte velocity)
+ {
+ digitalWrite(BLUELED, HIGH);
+ if (midiSettings.midiSoftThru)
+ {
+ sendNoteOnHW(note, velocity, channel);
+ }
+ if (midiSettings.midiInToCV)
+ {
+ cvNoteUtil.cvNoteOn(note);
+ }
+ omxScreensaver.resetCounter();
+ activeOmxMode->inMidiNoteOn(channel, note, velocity);
+ }
+
+ void handleNoteOff(byte channel, byte note, byte velocity)
+ {
+ digitalWrite(BLUELED, LOW);
+ if (midiSettings.midiSoftThru)
+ {
+ sendNoteOffHW(note, velocity, channel);
+ }
+ if (midiSettings.midiInToCV)
+ {
+ cvNoteUtil.cvNoteOff(note);
+ }
+ activeOmxMode->inMidiNoteOff(channel, note, velocity);
+ }
+
+ void handleControlChange(byte channel, byte control, byte value)
+ {
+ // digitalWrite(REDLED, HIGH);
+ if (midiSettings.midiSoftThru)
+ {
+ sendControlChangeHW(control, value, channel);
+ }
+ // change potbank on bank select
+ if (control == 0){
+ midiSettings.isBankSelect = true;
+ potSettings.potbank = constrain(value, 0, NUM_CC_BANKS - 1);
+ omxDisp.setDirty();
+ // }else if (midiSettings.isBankSelect && control == 32){
+ // midiSettings.isBankSelect = true;
+ }else{
+ midiSettings.isBankSelect = false;
+ }
+
+ // sendControlChange(control, value, channel);
+ }
+
+ // absolute_time_t last_ext_tick_at_ = 0;
+ // void externalMidiClockTick(absolute_time_t timestamp) {
+ // uint32_t delta = absolute_time_diff_us(last_ext_tick_at_, timestamp);
+ // if ( delta > 0) {
+ // clockConfig.ppqInterval = delta / 4 ;
+ // clockConfig.clockbpm = (60000000 / clockConfig.ppqInterval) / PPQ;
+
+ // last_ext_tick_at_ = timestamp;
+ // }
+ // }
+
+ // FIXME: This is debug stuff for incoming midi clock average.
+ // unsigned int cnt;
+ // int cntmax = 24;
+ void handleClock() {
+ // start a rolling average clock
+ // PPQN for MIDI is 24
+
+ // bool clockSource; // Internal clock (0), external clock (1)
+
+ if (sequencer.clockSource == 1){ // external clock
+
+ // absolute_time_t Now = time_us_32();
+ // externalMidiClockTick(Now);
+ // // omxDisp.setDirty();
+ // }
+
+/*
+ if (cnt == cntmax)
+ {
+ Serial.print("BPM: ");
+ Serial.print(clockstats.getBPM());
+ Serial.print(" Last Interval: ");
+ Serial.print(clockstats.getLastInterval());
+ Serial.print(" Samples: ");
+ Serial.println(clockstats.getSampleCount());
+ cnt = 0;
+ }
+ */
+ clockstats.clockPulse(micros());
+ clockConfig.clockbpm = clockstats.getBPM();
+ }
+
+ if (midiSettings.midiSoftThru){
+ // sendClock();
+ }
+ }
+
+ void handleStart() {
+ digitalWrite(REDLED, HIGH);
+ clockstats.start();
+ startTransport();
+ if (midiSettings.midiSoftThru){
+ }
+ }
+
+ void handleStop() {
+ digitalWrite(REDLED, LOW);
+ clockstats.stop();
+ stopTransport();
+ if (midiSettings.midiSoftThru){
+ }
+ }
+
+ void handleContinue() {
+ digitalWrite(REDLED, HIGH);
+ continueTransport();
+ if (midiSettings.midiSoftThru){
+ }
+ }
+
+ void OnSysEx(byte *sysexData, unsigned length)
+ {
+ sysEx->processIncomingSysex(sysexData, length);
+ }
+ void OnSysExHW(byte* sysexData, unsigned length)
+ {
+ sendSysEx(length, sysexData, false);
}
void sendNoteOn(int note, int velocity, int channel)
{
- usbMIDI.sendNoteOn(note, velocity, channel);
HWMIDI.sendNoteOn(note, velocity, channel);
+ usbMIDI.sendNoteOn(note, velocity, channel);
}
void sendNoteOnHW(int note, int velocity, int channel)
@@ -30,8 +218,8 @@ namespace MM
void sendNoteOff(int note, int velocity, int channel)
{
- usbMIDI.sendNoteOff(note, velocity, channel);
HWMIDI.sendNoteOff(note, velocity, channel);
+ usbMIDI.sendNoteOff(note, velocity, channel);
}
void sendNoteOffHW(int note, int velocity, int channel)
@@ -41,47 +229,58 @@ namespace MM
void sendControlChange(int control, int value, int channel)
{
- usbMIDI.sendControlChange(control, value, channel);
HWMIDI.sendControlChange(control, value, channel);
+ usbMIDI.sendControlChange(control, value, channel);
}
void sendControlChangeHW(int control, int value, int channel)
{
HWMIDI.sendControlChange(control, value, channel);
}
-
- void sendProgramChange(int program, int channel)
+
+ void sendProgramChange(byte program, byte channel)
{
- usbMIDI.sendProgramChange(program, channel);
+ // Bank switch?
HWMIDI.sendProgramChange(program, channel);
+ usbMIDI.sendProgramChange(program, channel);
}
void sendSysEx(uint32_t length, const uint8_t *sysexData, bool hasBeginEnd)
{
usbMIDI.sendSysEx(length, sysexData, hasBeginEnd);
+ HWMIDI.sendSysEx(length, sysexData, hasBeginEnd);
}
void sendClock()
{
- usbMIDI.sendRealTime(usbMIDI.Clock);
- HWMIDI.sendClock();
+ // usbMIDI.sendRealTime(midi::Clock);
+ if (sequencer.clockSource == 0){ // internal clock
+ usbMIDI.sendClock();
+ HWMIDI.sendClock();
+ }
}
- void startClock()
+ void startTransport()
{
- usbMIDI.sendRealTime(usbMIDI.Start);
+ // usbMIDI.sendRealTime(midi::Start);
+ // Serial.println("Start received");
+ usbMIDI.sendStart();
HWMIDI.sendStart();
}
- void continueClock()
+ void continueTransport()
{
- usbMIDI.sendRealTime(usbMIDI.Continue);
+ // usbMIDI.sendRealTime(midi::Continue);
+ // Serial.println("Continue received");
+ usbMIDI.sendContinue();
HWMIDI.sendContinue();
}
- void stopClock()
+ void stopTransport()
{
- usbMIDI.sendRealTime(usbMIDI.Stop);
+ // usbMIDI.sendRealTime(midi::Stop);
+ // Serial.println("Stop received");
+ usbMIDI.sendStop();
HWMIDI.sendStop();
}
diff --git a/OMX-27-firmware/src/midi/midi.h b/OMX-27-firmware/src/midi/midi.h
index 12cc9f0a..3863e2a2 100644
--- a/OMX-27-firmware/src/midi/midi.h
+++ b/OMX-27-firmware/src/midi/midi.h
@@ -1,6 +1,14 @@
#pragma once
#include
+#include
+
+#include "../consts/consts.h"
+#include "../modes/omx_mode_interface.h"
+
+#if BOARDTYPE == OMX2040
+#include // https://github.com/adafruit/Adafruit_TinyUSB_Arduino
+#endif
namespace MM
{
@@ -10,17 +18,34 @@ namespace MM
void sendNoteOn(int note, int velocity, int channel);
void sendNoteOff(int note, int velocity, int channel);
void sendControlChange(int control, int value, int channel);
- void sendProgramChange(int program, int channel);
+ void sendProgramChange(byte program, byte channel);
void sendNoteOnHW(int note, int velocity, int channel);
void sendNoteOffHW(int note, int velocity, int channel);
void sendControlChangeHW(int control, int value, int channel);
void sendSysEx(uint32_t length, const uint8_t *sysexData, bool hasBeginEnd);
+ // handlers/callbacks?
+ // void handleProgramChange(byte program, byte channel);
+ void handleNoteOn(byte channel, byte note, byte velocity);
+ void handleNoteOff(byte channel, byte note, byte velocity);
+ void handleControlChange(byte channel, byte number, byte value);
+
+ void OnControlChange(byte channel, byte number, byte value);
+ void OnSysEx(unsigned char *data, unsigned length);
+ void OnSysExHW(unsigned char *data, unsigned length);
+
+ void handleClock();
+ void handleStart();
+ void handleStop();
+ void handleContinue();
+
void sendClock();
- void startClock();
- void continueClock();
- void stopClock();
+ void startTransport();
+ void continueTransport();
+ void stopTransport();
bool usbMidiRead();
bool midiRead();
+
+
}
diff --git a/OMX-27-firmware/src/midi/noteoffs.cpp b/OMX-27-firmware/src/midi/noteoffs.cpp
index a503d226..b523ec10 100644
--- a/OMX-27-firmware/src/midi/noteoffs.cpp
+++ b/OMX-27-firmware/src/midi/noteoffs.cpp
@@ -1,5 +1,4 @@
#include "noteoffs.h"
-
#include
#include "../consts/consts.h"
#include "../config.h"
diff --git a/OMX-27-firmware/src/midi/sysex.cpp b/OMX-27-firmware/src/midi/sysex.cpp
index 4431eb32..7bb12b1f 100644
--- a/OMX-27-firmware/src/midi/sysex.cpp
+++ b/OMX-27-firmware/src/midi/sysex.cpp
@@ -1,13 +1,17 @@
+
+#include "../globals.h"
#include "../midi/sysex.h"
-#include "../midi/midi.h"
-#include "../config.h"
+
+// #include "../midi/midi.h"
+// #include "../config.h"
const uint8_t INFO = 0x1F;
const uint8_t CONFIG_EDIT = 0x0E;
const uint8_t CONFIG_DEVICE_EDIT = 0x0D;
-void SysEx::processIncomingSysex(const uint8_t *sysexData, unsigned size)
+void SysEx::processIncomingSysex(const byte *sysexData, unsigned size)
{
+ Serial.println("Sysex received");
if (size < 3)
{
// Serial.println("That's an empty sysex");
@@ -118,15 +122,22 @@ void SysEx::sendCurrentState()
sysexData[6] = MINOR_VERSION; // minor version
sysexData[7] = POINT_VERSION; // point version
- // 32 bytes of data:
- // EEPROM VERSION
- // MODE
- // PlayingPattern
- // MidiChannel
- // Pots (x25 - 5 banks of 5 pots)
- // 00
- // 00
- // 00
+ // X bytes of data:
+ // 0 - EEPROM VERSION
+ // 1 - Current MODE
+ // 2 - Sequencer PlayingPattern
+ // 3 - MIDI mode MidiChannel
+ // 4 - 28 - Pots (x25 - 5 banks of 5 pots)
+ // 29 - MIDI Macro Channel
+ // 30 - MIDI Macro Type
+ // 31 - Scale Root
+ // 32 - Scale Pattern, -1 for chromatic
+ // 33 - Lock Scale - Bool
+ // 34 - Scale Group 16 - Bool
+ // 35 - midiSettings.defaultVelocity
+ // 36 - clockConfig.globalQuantizeStepIndex
+ // 37 - cvNoteUtil.triggerMode
+ // 38 - actvie pot bank
uint8_t buffer[EEPROM_HEADER_SIZE];
this->storage->readArray(0, buffer, EEPROM_HEADER_SIZE);
diff --git a/OMX-27-firmware/src/midi/sysex.h b/OMX-27-firmware/src/midi/sysex.h
index 5a70be8d..d4fcd7ad 100644
--- a/OMX-27-firmware/src/midi/sysex.h
+++ b/OMX-27-firmware/src/midi/sysex.h
@@ -1,12 +1,13 @@
#pragma once
+// #include "../config.h"
#include "../hardware/storage.h"
-#include "../config.h"
+#include "midi.h"
class SysEx
{
- Storage *storage;
SysSettings *settings;
+ Storage *storage;
public:
SysEx(Storage *storage, SysSettings *settings) : storage(storage),
diff --git a/OMX-27-firmware/src/midifx/midifx_arpeggiator.cpp b/OMX-27-firmware/src/midifx/midifx_arpeggiator.cpp
index 63d37014..2f41052b 100644
--- a/OMX-27-firmware/src/midifx/midifx_arpeggiator.cpp
+++ b/OMX-27-firmware/src/midifx/midifx_arpeggiator.cpp
@@ -1,8 +1,10 @@
+#include "../globals.h"
#include "midifx_arpeggiator.h"
#include "../hardware/omx_disp.h"
#include "../utils/omx_util.h"
#include "../hardware/omx_leds.h"
#include "../consts/colors.h"
+
// #include "../sequencer.h"
#include
// #include
diff --git a/OMX-27-firmware/src/midifx/midifx_chord.cpp b/OMX-27-firmware/src/midifx/midifx_chord.cpp
index f74c0dcd..5d28c52a 100644
--- a/OMX-27-firmware/src/midifx/midifx_chord.cpp
+++ b/OMX-27-firmware/src/midifx/midifx_chord.cpp
@@ -441,9 +441,9 @@ namespace midifx
{
if (i > 0)
{
- tempString.append(" ");
+ tempString += " ";
}
- tempString.append(MusicScales::getFullNoteName(note));
+ tempString += (MusicScales::getFullNoteName(note));
}
}
diff --git a/OMX-27-firmware/src/midifx/midifx_repeat.cpp b/OMX-27-firmware/src/midifx/midifx_repeat.cpp
index d68b3215..5ca3e96c 100644
--- a/OMX-27-firmware/src/midifx/midifx_repeat.cpp
+++ b/OMX-27-firmware/src/midifx/midifx_repeat.cpp
@@ -1,4 +1,5 @@
#include "midifx_repeat.h"
+#include "../globals.h"
#include "../hardware/omx_disp.h"
#include "../utils/omx_util.h"
diff --git a/OMX-27-firmware/src/midimacro/midimacro_deluge.cpp b/OMX-27-firmware/src/midimacro/midimacro_deluge.cpp
index 166bfd6b..68dd435b 100644
--- a/OMX-27-firmware/src/midimacro/midimacro_deluge.cpp
+++ b/OMX-27-firmware/src/midimacro/midimacro_deluge.cpp
@@ -1,4 +1,5 @@
#include "midimacro_deluge.h"
+#include "../globals.h"
#include "../utils/omx_util.h"
#include "../hardware/omx_disp.h"
#include "../hardware/omx_leds.h"
@@ -126,10 +127,10 @@ namespace midimacro
paramBanks[15].SetCCs("Pot 1", 105, "Pot 2", 106, "Pot 3", 107, "Pot 4", 108, "Pot 5", 109);
- params_.addPage(5); //
- // params_.addPage(1); //
- // params_.addPage(1); //
- // params_.addPage(1); //
+ params_.addPage(5); //
+ // params_.addPage(1); //
+ // params_.addPage(1); //
+ // params_.addPage(1); //
for(uint8_t i = 0; i < 127; i++)
{
@@ -184,7 +185,7 @@ namespace midimacro
{
if (bankIndex >= kNumBanks)
{
- Serial.println((String)"ERROR:MidiMacroDeluge: Cannot set active bank to " + bankIndex);
+// Serial.println((String)"ERROR:MidiMacroDeluge: Cannot set active bank to " + bankIndex);
return;
}
@@ -197,7 +198,7 @@ namespace midimacro
if(params_.getSelPage() == 0)
{
- params_.setSelParam(activeParam);
+ params_.setSelParam(activeParam);
}
updatePotPickups();
@@ -208,7 +209,7 @@ namespace midimacro
// Updates the pot pickups to the values saved in the active bank
// Thus if we switch banks, the value will need to be picked up
- // by the pot before it sends out to avoid jumping values.
+ // by the pot before it sends out to avoid jumping values.
void MidiMacroDeluge::updatePotPickups()
{
auto activeBank = getActiveBank();
@@ -296,7 +297,7 @@ namespace midimacro
// if(activeBank != nullptr)
// {
-
+
// }
// Serial.println((String)"IN CC: " + control + " VAL: " + value); // 5968
@@ -339,7 +340,7 @@ namespace midimacro
// omxDisp.displayMessageTimed(String(activeBank->paramNames[potIndex]) + " " + String(delugeMapVal), 5);
MM::sendControlChange(cc, potPickups[potIndex].value, midiMacroConfig.midiMacroChan);
}
- else
+ else
{
// uint8_t delugeMapNewVal = (uint8_t)map(newValue, 0, 127, 0, 50);
// uint8_t delugeMapVal = (uint8_t)map(potPickups[potIndex].value, 0, 127, 0, 50);
@@ -409,7 +410,7 @@ namespace midimacro
}
}
// Change Octave
- else if (thisKey == 11 || thisKey == 12)
+ else if (thisKey == 11 || thisKey == 12)
{
int amt = thisKey == 11 ? -1 : 1;
midiSettings.octave = constrain(midiSettings.octave + amt, -5, 4);
@@ -587,7 +588,7 @@ namespace midimacro
void MidiMacroDeluge::onEncoderChangedSelectParam(Encoder::Update enc)
{
params_.changeParam(enc.dir());
-
+
if(params_.getSelPage() == 0)
{
activeParam = params_.getSelParam();
@@ -666,7 +667,7 @@ namespace midimacro
// // omxDisp.displayMessageTimed(String(activeBank->paramNames[potIndex]) + " " + String(delugeMapVal), 5);
// MM::sendControlChange(cc, potPickups[potIndex].value, midiMacroConfig.midiMacroChan);
// }
- // else
+ // else
// {
// uint8_t delugeMapNewVal = (uint8_t)map(newValue, 0, 127, 0, 50);
// uint8_t delugeMapVal = (uint8_t)map(potPickups[potIndex].value, 0, 127, 0, 50);
@@ -677,7 +678,7 @@ namespace midimacro
uint8_t delugeMapPotVal = (uint8_t)map(potPickups[activeParam].potValue, 0, 127, 0, 50);
uint8_t delugeMapVal = (uint8_t)map(potPickups[activeParam].value, 0, 127, 0, 50);
-
+
omxDisp.dispParamBar(delugeMapPotVal, delugeMapVal, 0, 50, potPickups[activeParam].pickedUp, false, activeBank->bankName, activeBank->paramNames[activeParam]);
// omxDisp.dispGenericModeLabel(activeBank->bankName, params_.getNumPages(), params_.getSelPage());
diff --git a/OMX-27-firmware/src/midimacro/midimacro_m8.cpp b/OMX-27-firmware/src/midimacro/midimacro_m8.cpp
index 5502dce4..37eb449d 100644
--- a/OMX-27-firmware/src/midimacro/midimacro_m8.cpp
+++ b/OMX-27-firmware/src/midimacro/midimacro_m8.cpp
@@ -1,4 +1,5 @@
#include "midimacro_m8.h"
+#include "../globals.h"
#include "../utils/omx_util.h"
#include "../hardware/omx_disp.h"
#include "../hardware/omx_leds.h"
diff --git a/OMX-27-firmware/src/midimacro/midimacro_norns.cpp b/OMX-27-firmware/src/midimacro/midimacro_norns.cpp
index 5c72ccba..bc2cb9f8 100644
--- a/OMX-27-firmware/src/midimacro/midimacro_norns.cpp
+++ b/OMX-27-firmware/src/midimacro/midimacro_norns.cpp
@@ -1,4 +1,5 @@
#include "midimacro_norns.h"
+#include "../globals.h"
#include "../utils/omx_util.h"
#include "../hardware/omx_disp.h"
#include "../hardware/omx_leds.h"
diff --git a/OMX-27-firmware/src/modes/euclidean_sequencer.cpp b/OMX-27-firmware/src/modes/euclidean_sequencer.cpp
index 4af1db76..47e75678 100644
--- a/OMX-27-firmware/src/modes/euclidean_sequencer.cpp
+++ b/OMX-27-firmware/src/modes/euclidean_sequencer.cpp
@@ -336,7 +336,7 @@ namespace euclidean
{
sOut += (pattern_[i] ? "X" : "-");
}
- Serial.println(sOut.c_str());
+// Serial.println(sOut.c_str());
}
EuclidSave EuclideanSequencer::getSave()
{
diff --git a/OMX-27-firmware/src/modes/omx_mode_chords.cpp b/OMX-27-firmware/src/modes/omx_mode_chords.cpp
index 9c2626b9..5ff3d8be 100644
--- a/OMX-27-firmware/src/modes/omx_mode_chords.cpp
+++ b/OMX-27-firmware/src/modes/omx_mode_chords.cpp
@@ -1,5 +1,6 @@
#include "omx_mode_chords.h"
#include "../config.h"
+#include "../globals.h"
#include "../consts/colors.h"
#include "../utils/omx_util.h"
#include "../utils/cvNote_util.h"
@@ -2825,9 +2826,9 @@ void OmxModeChords::onDisplayUpdate()
{
if (i > 0)
{
- notesString.append(" ");
+ notesString += " ";
}
- notesString.append(musicScale_->getFullNoteName(note));
+ notesString += (musicScale_->getFullNoteName(note));
// if(i < 4)
// {
diff --git a/OMX-27-firmware/src/modes/omx_mode_chords.h b/OMX-27-firmware/src/modes/omx_mode_chords.h
index c6a521d0..279f1478 100644
--- a/OMX-27-firmware/src/modes/omx_mode_chords.h
+++ b/OMX-27-firmware/src/modes/omx_mode_chords.h
@@ -1,8 +1,10 @@
#pragma once
+
#include "../modes/omx_mode_interface.h"
#include "../utils/music_scales.h"
// #include "../consts/colors.h"
#include "../config.h"
+#include "../globals.h"
// #include "../modes/omx_mode_midi_keyboard.h"
#include "../utils/param_manager.h"
#include "../hardware/storage.h"
diff --git a/OMX-27-firmware/src/modes/omx_mode_drum.cpp b/OMX-27-firmware/src/modes/omx_mode_drum.cpp
index 59d16506..875fbcbc 100644
--- a/OMX-27-firmware/src/modes/omx_mode_drum.cpp
+++ b/OMX-27-firmware/src/modes/omx_mode_drum.cpp
@@ -1,5 +1,6 @@
#include "omx_mode_drum.h"
#include "../config.h"
+#include "../globals.h"
#include "../consts/colors.h"
#include "../utils/omx_util.h"
#include "../utils/cvNote_util.h"
diff --git a/OMX-27-firmware/src/modes/omx_mode_euclidean.cpp b/OMX-27-firmware/src/modes/omx_mode_euclidean.cpp
index f9a42914..c5e3d097 100644
--- a/OMX-27-firmware/src/modes/omx_mode_euclidean.cpp
+++ b/OMX-27-firmware/src/modes/omx_mode_euclidean.cpp
@@ -347,14 +347,23 @@ void OmxModeEuclidean::onPotChanged(int potIndex, int prevValue, int newValue, i
EuclideanSequencer *activeEuclid = &euclids[selectedEuclid_];
// Serial.println(String("PotChanged ") + String(potIndex));
+ // Serial.println(String("newValue ") + String(newValue));
+ // Serial.println(String("analogDelta ") + String(analogDelta));
// --- EDIT MODE ---
if (paramMode_ == PARAMMODE_EDIT)
{
// Serial.println("Edit Mode");
+#if (BOARDTYPE == TEENSY4 || BOARDTYPE == OMX2040)
+ // Assuming delta also small for T4
+ // Small delta on OMX2040
+ if (analogDelta < 1)
+ return;
+#else
if (analogDelta < 3)
return;
+#endif
if (potIndex == 0)
{
diff --git a/OMX-27-firmware/src/modes/omx_mode_grids.cpp b/OMX-27-firmware/src/modes/omx_mode_grids.cpp
index cf929390..05c06c8e 100644
--- a/OMX-27-firmware/src/modes/omx_mode_grids.cpp
+++ b/OMX-27-firmware/src/modes/omx_mode_grids.cpp
@@ -1,5 +1,6 @@
#include "omx_mode_grids.h"
#include "../config.h"
+#include "../globals.h"
#include "../utils/omx_util.h"
#include "../hardware/omx_disp.h"
#include "../hardware/omx_leds.h"
@@ -75,7 +76,7 @@ void OmxModeGrids::onClockTick()
void OmxModeGrids::onPotChanged(int potIndex, int prevValue, int newValue, int analogDelta)
{
-#if T4
+#if (BOARDTYPE == TEENSY4 || BOARDTYPE == OMX2040)
int deltaTheshold = 1;
#else
int deltaTheshold = 6;
@@ -91,7 +92,7 @@ void OmxModeGrids::onPotChanged(int potIndex, int prevValue, int newValue, int a
// if (analogDelta < 3)
// return;
-#if T4
+#if (BOARDTYPE == TEENSY4 || BOARDTYPE == OMX2040)
// prevents values from being modified until pot is modified
if (potPostLoadThresh[potIndex])
{
@@ -416,7 +417,7 @@ void OmxModeGrids::onEncoderChanged(Encoder::Update enc)
if (noteLength != newNoteLength)
{
grids_.setNoteLength(lockedInst_, newNoteLength);
- omxDisp.displayMessage(kNoteLengths[newNoteLength]);
+ omxDisp.displayMessage((String) kNoteLengths[newNoteLength]);
omxDisp.setDirty();
}
}
@@ -543,8 +544,8 @@ void OmxModeGrids::loadActivePattern(uint8_t pattIndex)
void OmxModeGrids::startPlayback()
{
gridsAUX = true;
- omxUtil.resetClocks();
grids_.start();
+ omxUtil.resetClocks();
omxUtil.startClocks();
// sequencer.playing = true;
isPlaying_ = true;
diff --git a/OMX-27-firmware/src/modes/omx_mode_midi_keyboard.cpp b/OMX-27-firmware/src/modes/omx_mode_midi_keyboard.cpp
index b9d82c61..b707a941 100644
--- a/OMX-27-firmware/src/modes/omx_mode_midi_keyboard.cpp
+++ b/OMX-27-firmware/src/modes/omx_mode_midi_keyboard.cpp
@@ -1,5 +1,6 @@
#include "omx_mode_midi_keyboard.h"
#include "../config.h"
+#include "../globals.h"
#include "../consts/colors.h"
#include "../utils/omx_util.h"
#include "../utils/cvNote_util.h"
@@ -23,8 +24,7 @@ enum MIKeyModePage {
MIPAGE_POTSANDMACROS,
MIPAGE_SCALES,
MIPAGE_CFG,
- MIPAGE_CLOCK_SOURCE,
- MIPAGE_CLOCK_SEND,
+ MIPAGE_CLOCK_SOURCE,
MIPAGE_VERSION
};
@@ -37,6 +37,7 @@ OmxModeMidiKeyboard::OmxModeMidiKeyboard()
params.addPage(4); // PotBank, Thru, Macro, Macro Channel
params.addPage(4); // Root, Scale, Lock Scale Notes, Group notes.
params.addPage(4); // Pot CC CFG
+ params.addPage(4); // MIPAGE_CLOCK_SOURCE
params.addPage(4); // MIPAGE_VERSION
// subModeMidiFx.setNoteOutputFunc(&OmxModeMidiKeyboard::onNotePostFXForwarder, this);
@@ -476,7 +477,7 @@ void OmxModeMidiKeyboard::onEncoderChanged(Encoder::Update enc)
cvNoteUtil.triggerMode = constrain(cvNoteUtil.triggerMode + amt, 0, 1);
}
}
- else if (selPage == MIPAGE_CLOCK_SOURCE)
+ else if (selPage == MIPAGE_CLOCK_SOURCE)
{
if (selParam == 1)
{
@@ -488,7 +489,6 @@ void OmxModeMidiKeyboard::onEncoderChanged(Encoder::Update enc)
}
}
-
omxDisp.setDirty();
}
@@ -1238,13 +1238,13 @@ void OmxModeMidiKeyboard::onDisplayUpdate()
omxDisp.setLegend(2,"QUANT", "1/" + String(kArpRates[clockConfig.globalQuantizeStepIndex]));
omxDisp.setLegend(3,"CV M", cvNoteUtil.getTriggerModeDispName());
}
- else if (params.getSelPage() == MIPAGE_CLOCK_SOURCE)
- {
+ else if (params.getSelPage() == MIPAGE_CLOCK_SOURCE) {
omxDisp.clearLegends();
omxDisp.setLegend(0,"CLKS", sequencer.clockSource ? "Ext" : "Int");
omxDisp.setLegend(1,"SEND", clockConfig.send_always ? "ON" : "OFF"); // Always send clock or not
}
+
omxDisp.dispGenericMode2(params.getNumPages(), params.getSelPage(), params.getSelParam(), encoderSelect && !midiSettings.midiAUX);
}
}
@@ -1345,7 +1345,6 @@ void OmxModeMidiKeyboard::SetScale(MusicScales *scale)
m8Macro_.setScale(scale);
nornsMarco_.setScale(scale);
}
-
void OmxModeMidiKeyboard::sendMidiClock(bool send)
{
clockConfig.send_always = !clockConfig.send_always;
diff --git a/OMX-27-firmware/src/modes/omx_mode_sequencer.cpp b/OMX-27-firmware/src/modes/omx_mode_sequencer.cpp
index 0b9789f0..8850e05b 100644
--- a/OMX-27-firmware/src/modes/omx_mode_sequencer.cpp
+++ b/OMX-27-firmware/src/modes/omx_mode_sequencer.cpp
@@ -1,5 +1,6 @@
#include "omx_mode_sequencer.h"
#include "../config.h"
+#include "../globals.h"
#include "../consts/colors.h"
#include "../utils/omx_util.h"
#include "../hardware/omx_disp.h"
diff --git a/OMX-27-firmware/src/modes/omx_screensaver.cpp b/OMX-27-firmware/src/modes/omx_screensaver.cpp
index f3cf5e19..0798dadb 100644
--- a/OMX-27-firmware/src/modes/omx_screensaver.cpp
+++ b/OMX-27-firmware/src/modes/omx_screensaver.cpp
@@ -1,4 +1,5 @@
#include "omx_screensaver.h"
+#include "../globals.h"
#include "../consts/consts.h"
#include "../config.h"
#include "../utils/omx_util.h"
@@ -7,12 +8,13 @@
void OmxScreensaver::setScreenSaverColor()
{
- colorConfig.screensaverColor = map(potSettings.analog[4]->getValue(), potMinVal, potMaxVal, 0, ssMaxColorDepth);
+ int tempcolor = potSettings.analog[4]->getValue();
+ colorConfig.screensaverColor = map(tempcolor, potMinVal, potMaxVal, 0, ssMaxColorDepth);
+ // Serial.println(colorConfig.screensaverColor);
}
void OmxScreensaver::onPotChanged(int potIndex, int prevValue, int newValue, int analogDelta)
{
- // set screensaver color with pot 4
if (potSettings.analog[4]->hasChanged())
{
setScreenSaverColor();
@@ -28,7 +30,7 @@ void OmxScreensaver::updateScreenSaverState()
{
if (screenSaverCounter > screensaverInterval)
{
- if (!screenSaverActive)
+ if (!screenSaverActive)
{
screenSaverActive = true;
setScreenSaverColor();
@@ -51,6 +53,7 @@ void OmxScreensaver::updateScreenSaverState()
bool OmxScreensaver::shouldShowScreenSaver()
{
+ // setScreenSaverColor();
return screenSaverActive;
}
@@ -67,12 +70,10 @@ void OmxScreensaver::onDisplayUpdate()
updateLEDs();
omxDisp.clearDisplay();
}
-
void OmxScreensaver::resetCounter()
{
screenSaverCounter = 0;
}
-
void OmxScreensaver::updateLEDs()
{
unsigned long playstepmillis = millis();
@@ -83,13 +84,18 @@ void OmxScreensaver::updateLEDs()
int j = 26 - ssloop;
int i = ssstep + 11;
+ int saturation = 255;
+ int brightness = 255;
for (int z = 1; z < 11; z++)
{
- strip.setPixelColor(z, 0);
+ strip.setPixelColor(z, 0, 0, 0 );
}
if (colorConfig.screensaverColor < ssMaxColorDepth)
{
+ if (colorConfig.screensaverColor > 65200){
+ brightness = 0;
+ }
if (!ssreverse)
{
// turn off all leds
@@ -97,14 +103,14 @@ void OmxScreensaver::updateLEDs()
{
if (i < j)
{
- strip.setPixelColor(x + 11, 0);
+ strip.setPixelColor(x + 11, 0, 0, 0 );
}
if (x + 11 > j)
{
- strip.setPixelColor(x + 11, strip.gamma32(strip.ColorHSV(colorConfig.screensaverColor)));
+ strip.setPixelColor(x + 11, strip.gamma32(strip.ColorHSV(colorConfig.screensaverColor, saturation, brightness)));
}
}
- strip.setPixelColor(i + 1, strip.gamma32(strip.ColorHSV(colorConfig.screensaverColor)));
+ strip.setPixelColor(i + 1, strip.gamma32(strip.ColorHSV(colorConfig.screensaverColor, saturation, brightness)));
}
else
{
@@ -112,21 +118,21 @@ void OmxScreensaver::updateLEDs()
{
if (i >= j)
{
- strip.setPixelColor(y + 11, 0);
+ strip.setPixelColor(y + 11, 0, 0, 0 );
}
if (y + 11 < j)
{
- strip.setPixelColor(y + 11, strip.gamma32(strip.ColorHSV(colorConfig.screensaverColor)));
+ strip.setPixelColor(y + 11, strip.gamma32(strip.ColorHSV(colorConfig.screensaverColor, saturation, brightness)));
}
}
- strip.setPixelColor(i + 1, strip.gamma32(strip.ColorHSV(colorConfig.screensaverColor)));
+ strip.setPixelColor(i + 1, strip.gamma32(strip.ColorHSV(colorConfig.screensaverColor, saturation, brightness)));
}
}
else
{
for (int w = 0; w < 27; w++)
{
- strip.setPixelColor(w, 0);
+ strip.setPixelColor(w, 0, 0, 0 );
}
}
ssstep++;
diff --git a/OMX-27-firmware/src/modes/omx_screensaver.h b/OMX-27-firmware/src/modes/omx_screensaver.h
index b7abd7b3..ed2e50ad 100644
--- a/OMX-27-firmware/src/modes/omx_screensaver.h
+++ b/OMX-27-firmware/src/modes/omx_screensaver.h
@@ -1,6 +1,7 @@
#pragma once
#include "./omx_mode_interface.h"
+#include
class OmxScreensaver : public OmxModeInterface
{
@@ -30,9 +31,12 @@ class OmxScreensaver : public OmxModeInterface
private:
void setScreenSaverColor();
elapsedMillis screenSaverCounter = 0;
- unsigned long screensaverInterval = 1000 * 60 * 3; // 3 minutes default
- uint32_t ssMaxColorDepth = 65528; // used by setScreenSaverColor(). Allows for full rainbow of colors, plus a little extra for 'black'
+ unsigned long screensaverInterval = 1000 * 60 * 3; // 3 minutes default? // 10000; 15000; //
+ uint32_t ssMaxColorDepth = 65528; // used by setScreenSaverColor().
+ // Allows for full rainbow of colors, plus a little extra for 'black'
+ // Uncomment for 10 second screensaver for testing
+ // unsigned long screensaverInterval = 1000 * 10;
int ssstep = 0;
int ssloop = 0;
volatile unsigned long nextStepTimeSS = 0;
diff --git a/OMX-27-firmware/src/modes/retro_grids.cpp b/OMX-27-firmware/src/modes/retro_grids.cpp
index d2db09f5..5ec4fdd3 100644
--- a/OMX-27-firmware/src/modes/retro_grids.cpp
+++ b/OMX-27-firmware/src/modes/retro_grids.cpp
@@ -451,7 +451,7 @@ namespace grids
{
tickCount_ = 0;
running_ = true;
-// MM::startClock();
+ // MM::startClock();
nextStepTimeP_ = micros();
lastStepTimeP_ = micros();
@@ -460,13 +460,13 @@ namespace grids
void GridsWrapper::stop()
{
running_ = false;
-// MM::stopClock();
+ // MM::stopClock();
}
void GridsWrapper::proceed()
{
running_ = true;
- MM::continueClock();
+ MM::continueTransport();
}
void GridsWrapper::setNoteOutputFunc(void (*fptr)(void *, uint8_t, MidiNoteGroup), void *context)
diff --git a/OMX-27-firmware/src/modes/sequencer.cpp b/OMX-27-firmware/src/modes/sequencer.cpp
index a2442386..04fe17fb 100644
--- a/OMX-27-firmware/src/modes/sequencer.cpp
+++ b/OMX-27-firmware/src/modes/sequencer.cpp
@@ -1,6 +1,7 @@
#include
#include "sequencer.h"
+#include "../globals.h"
#include "../config.h"
#include "../consts/consts.h"
#include "../consts/colors.h"
@@ -733,7 +734,9 @@ void allNotesOff()
void allNotesOffPanic()
{
-#if T4
+#if BOARDTYPE == TEENSY4
+ dac.setVoltage(0, false);
+#elif BOARDTYPE == OMX2040
dac.setVoltage(0, false);
#else
analogWrite(CVPITCH_PIN, 0);
@@ -775,8 +778,8 @@ void seqReset()
sequencer.lastSeqPos[k] = sequencer.seqPos[k];
}
}
-// omxUtil.stopClocks();
-// omxUtil.startClocks();
+ // omxUtil.stopClocks();
+ // omxUtil.startClocks();
// MM::stopClock();
// MM::startClock();
sequencer.seqResetFlag = false;
@@ -797,7 +800,9 @@ void seqStart()
{
omxUtil.resumeClocks();
// MM::continueClock();
-// } else if (sequencer.seqPos[sequencer.playingPattern]==0) {
+ // } else if (seqPos[sequencer.playingPattern]==0) {
+ // MM::startClock();
+ // } else if (sequencer.seqPos[sequencer.playingPattern]==0) {
} else {
omxUtil.startClocks();
// MM::startClock();
diff --git a/OMX-27-firmware/src/modes/submodes/submode_potconfig.cpp b/OMX-27-firmware/src/modes/submodes/submode_potconfig.cpp
index c70931bf..377d3aa1 100644
--- a/OMX-27-firmware/src/modes/submodes/submode_potconfig.cpp
+++ b/OMX-27-firmware/src/modes/submodes/submode_potconfig.cpp
@@ -1,3 +1,5 @@
+
+#include "../../globals.h"
#include "submode_potconfig.h"
#include "../../hardware/omx_disp.h"
#include "../../hardware/omx_leds.h"
diff --git a/OMX-27-firmware/src/utils/cvNote_util.cpp b/OMX-27-firmware/src/utils/cvNote_util.cpp
index 0d952c2c..4cea5588 100644
--- a/OMX-27-firmware/src/utils/cvNote_util.cpp
+++ b/OMX-27-firmware/src/utils/cvNote_util.cpp
@@ -186,18 +186,20 @@ void CVNoteUtil::setGate(bool high)
// Serial.println("SetGate: " + String(high));
// Serial.println("notes Size: " + String(cvNotes_.size()));
-
+
digitalWrite(CVGATE_PIN, high);
}
void CVNoteUtil::setPitch(uint8_t cvNoteNum)
{
cvPitch = static_cast(roundf(cvNoteNum * stepsPerSemitone)); // map (adjnote, 36, 91, 0, 4080);
-#if T4
+#if BOARDTYPE == TEENSY4
dac.setVoltage(cvPitch, false);
+#elif BOARDTYPE == OMX2040
+ dac.setVoltage(cvPitch, false);
#else
analogWrite(CVPITCH_PIN, cvPitch);
#endif
}
-CVNoteUtil cvNoteUtil;
\ No newline at end of file
+CVNoteUtil cvNoteUtil;
diff --git a/OMX-27-firmware/src/utils/logic_util.h b/OMX-27-firmware/src/utils/logic_util.h
index edbcf547..eba43199 100644
--- a/OMX-27-firmware/src/utils/logic_util.h
+++ b/OMX-27-firmware/src/utils/logic_util.h
@@ -1,8 +1,25 @@
#pragma once
+#ifndef WRAP
#define WRAP(a, b) ((b) + ((a) % (b))) % (b)
+#endif
+
+#ifndef ARRAYLEN
#define ARRAYLEN(x) (sizeof(x) / sizeof(x[0]))
+#endif
+
+#ifndef SGN
#define SGN(x) ((x) < 0 ? -1 : 1)
+#endif
+
+#ifndef MIN
#define MIN(a, b) ((a) < (b) ? (a) : (b))
+#endif
+
+#ifndef MAX
#define MAX(a, b) ((a) > (b) ? (a) : (b))
+#endif
+
+#ifndef CLAMP
#define CLAMP(a, min, max) (MAX(MIN(a, max), min))
+#endif
diff --git a/OMX-27-firmware/src/utils/omx_util.cpp b/OMX-27-firmware/src/utils/omx_util.cpp
index 82e22373..f64308d7 100644
--- a/OMX-27-firmware/src/utils/omx_util.cpp
+++ b/OMX-27-firmware/src/utils/omx_util.cpp
@@ -1,7 +1,8 @@
#include
-#include "omx_util.h"
+#include "../globals.h"
#include "../consts/consts.h"
+#include "omx_util.h"
#include "../midi/midi.h"
#include "../consts/colors.h"
#include "../hardware/omx_leds.h"
@@ -34,7 +35,7 @@ float OmxUtil::lerp(float a, float b, float t)
void OmxUtil::advanceClock(OmxModeInterface *activeOmxMode, Micros advance)
{
- // advance is delta in Micros from previous loop update to this loop update.
+ // advance is delta in Micros from previous loop update to this loop update.
// XXXXXXXXXXXXXXXXXXXXXXXX
// Txxxxxxxxxxxxxxxxxxxxxxx - Quarter Note - 24 ticks
@@ -55,7 +56,7 @@ void OmxUtil::advanceClock(OmxModeInterface *activeOmxMode, Micros advance)
// in a while loop like this is
// Maybe so if there is a long advance multiple clocks
// will get fired to catch up?
- // Keeping like this for now as it works.
+ // Keeping like this for now as it works.
while (adv >= timeToNextClock)
{
adv -= timeToNextClock;
@@ -79,10 +80,12 @@ void OmxUtil::advanceClock(OmxModeInterface *activeOmxMode, Micros advance)
{
// Should always send clock
// This way external gear can update themselves
+ // MM::sendClock();
if (clockConfig.send_always)
{
MM::sendClock();
}
+
}
if (activeOmxMode_ != nullptr)
@@ -139,7 +142,7 @@ void OmxUtil::resetClocks()
{
// BPM tempo to step_delay calculation
// 60000000 = 60 secs
- clockConfig.ppqInterval = 60000000 / (PPQ * clockConfig.clockbpm); // ppq interval is in microseconds, 96 * 120 = 11520, 60000000 / 11520 = 52083 microsecond, * 0.001 = 5.208 milliseconds,
+ clockConfig.ppqInterval = 60000000 / (PPQ * clockConfig.clockbpm); // ppq interval is in microseconds, 96 * 120 = 11520, 60000000 / 11520 = 5208.3 microsecond, * 0.001 = 5.208 milliseconds,
clockConfig.step_micros = clockConfig.ppqInterval * (PPQ / 4); // 16th note step in microseconds (quarter of quarter note)
// 16th note step length in milliseconds
@@ -158,21 +161,21 @@ void OmxUtil::startClocks()
{
sendClocks_ = true;
clockConfig.send_always = true;
- MM::startClock();
+ MM::startTransport();
}
void OmxUtil::resumeClocks()
{
sendClocks_ = true;
clockConfig.send_always = true;
- MM::continueClock();
+ MM::continueTransport();
}
void OmxUtil::stopClocks()
{
sendClocks_ = false;
clockConfig.send_always = false;
- MM::stopClock();
+ MM::stopTransport();
}
bool OmxUtil::areClocksRunning()
@@ -187,7 +190,7 @@ bool OmxUtil::areClocksRunning()
// midiSettings.pitchCV = static_cast(roundf((notenum - cvLowestNote) * stepsPerSemitone)); // map (adjnote, 36, 91, 0, 4080);
// digitalWrite(CVGATE_PIN, HIGH);
// // analogWrite(CVPITCH_PIN, midiSettings.pitchCV);
-// #if T4
+// #if BOARDTYPE == TEENSY4
// dac.setVoltage(midiSettings.pitchCV, false);
// #else
// analogWrite(CVPITCH_PIN, midiSettings.pitchCV);
@@ -555,8 +558,8 @@ void OmxUtil::onEncoderChangedEditParam(Encoder::Update *enc, MusicScales *music
case GPARAM_POTS_LASTVAL:
case GPARAM_POTS_LASTCC:
{
- Serial.println("Param not editable: ");
- Serial.println(paramType);
+// Serial.println("Param not editable: ");
+// Serial.println(paramType);
}
break;
}
@@ -597,13 +600,13 @@ void OmxUtil::setupPageLegend(MusicScales *musicScale, uint8_t index, uint8_t pa
break;
case GPARAM_MIDI_LASTNOTE:
{
- omxDisp.legends[index] = "NOTE";
+ omxDisp.legends[index] = "NOTE";
omxDisp.legendVals[index] = midiSettings.midiLastNote;
}
break;
case GPARAM_MIDI_LASTVEL:
{
- omxDisp.legends[index] = "VEL";
+ omxDisp.legends[index] = "VEL";
omxDisp.legendVals[index] = midiSettings.midiLastVel;
}
break;
@@ -666,6 +669,18 @@ void OmxUtil::setupPageLegend(MusicScales *musicScale, uint8_t index, uint8_t pa
omxDisp.legendText[index] = macromodes[midiMacroConfig.midiMacro];
}
break;
+ case GPARAM_CLOCK_SOURCE:
+ {
+ omxDisp.legends[index] = "CLKS";
+ omxDisp.legendText[index] = sequencer.clockSource ? "Int" : "Ext";
+ }
+ break;
+ case GPARAM_CLOCK_SEND:
+ {
+ omxDisp.legends[index] = "SEND";
+ omxDisp.legendText[index] = clockConfig.send_always ? "ON" : "OFF";
+ }
+ break;
case GPARAM_MACRO_CHAN:
{
omxDisp.legends[index] = "M-CH";
diff --git a/README.md b/README.md
index a44c0d54..e80f54c9 100644
--- a/README.md
+++ b/README.md
@@ -1,62 +1,48 @@
-# OMX-27
+# OMX-27 version 3

-Mechanical key switch midi keyboard and sequencer. Based on Teensy 3.2 and Cherry MX RGB key switches.
+OMX-27 is a compact DIY hardware MIDI controller and sequencer with RGB LED backlit mechanical key switches.
-Full kits and partial kits are [available for sale here](https://www.denki-oto.com/).
-
-Dimensions: 313mm x 65mm
-
-## Firmware
+Version 3 of the hardware is based on the RP2040 microprocessor
+Kits and specs are [available here](https://www.denki-oto.com/).
-Kits are shipped with a blank Teensy. You will need to flash the firmware to the device.
+Information and code for earlier heardware versions is included in the [Archive](Archive/ReadMe.md) directory.
-### Load pre-compiled firmware w/ TyUpdater
-
-Download the correct OMX-27 firmware "hex" file from the [GitHub Releases page](https://github.com/okyeron/OMX-27/releases) page or the Firmware-Hexes directory in this repo.
+Dimensions: 313mm x 65mm
-NOTE - 2023 boards with Teensy 4.0 have a different firmware and will have a "T4" suffix.
+## Firmware
-Get TyTools [from GitHub here](https://github.com/Koromix/tytools/releases). More info here (https://koromix.dev/tytools).
+Kits are shipped with the current firmware already flashed.
-Copy TyUploader to your machine and open it. Be sure your OMX-27 is plugged in. It should show up in the TyUpdater application.
+The OMX-27 firmware is Open Source and current uses PlatformIO (Arduino)
-
+See [below](<#Firmware-Development>) to compile the firmware yourself.
-Click the Upload button and select the firmware hex file you want to upload. This should upload the firmware and the OMX-27 should reboot. That's it.
+## Build
+[Build Guide]()
-### Teensyduino (compile yourself)
+## Docs
-Install Teensyduino from the [PJRC website](https://www.pjrc.com/teensy/teensyduino.html).
+[Documentation]()
-In Teensyduino Library Manager - check to be sure these are installed and on the most recent versions.
+## Web Configurator
-__Libraries:__
-Adafruit_Keypad
-Adafruit_NeoPixel
-Adafruit_SSD1306
-Adafruit_GFX_Library
-U8g2_for_Adafruit_GFX
-Adafruit_FRAM_I2C
-Adafruit_MCP4725
+[Online Configurator](https://okyeron.github.io/OMX-27/webconfig/index.html)
-
+## BOM
+OMX-27 v3 comes with SMD parts pre-assembled. Some alternate thru-hole parts are listed in the [Bill of Materials]() for reference
-Also check to be sure MIDI Library (by Francois Best / fortyseveneffects) is updated to 5.02 (I believe this is installed by default with Teensyduino)
-Set the following for the Teensy under the Tools menu:
-__Board: Teensy 3.2/3.1__ or __Board: Teensy 4.0__
-__USB Type: MIDI__
-__CPU Speed: 120 MHz (overclock)__
+## Firmware Development
-Open the sketch at `OMX-27-firmware/OMX-27-firmware.ino`, click verify to ensure it all compiles and upload to flash the firmware to the hardware, pushing the button on the Teensy first.
+Currently the firmware is best worked on with PlatformIO and VSCode. This may change in future.
-### PlatformIO / VSCode (optional)
+### PlatformIO / VSCode
Ensure Homebrew in installed. [Instructions](https://brew.sh/)
Install PlatformIO CLI tools. [Detailed Instructions](https://platformio.org/install/cli)
@@ -69,59 +55,38 @@ brew install platformio
git checkout https://github.com/okyeron/OMX-27.git
# go to the project directory
-cd OMX-27
+cd OMX-27-RP2040
# compile the project (this may take a while the first time)
pio run
-# upload to hardware (don't forget to push button on Teensy)
+# upload to hardware (press reset and boot and release reset before boot)
pio run -t upload
-
-# use serial monitor for debugging
-pio device monitor
-
-# clear FRAM/EEPROM
-pio run -t clear-storage
```
(optional) Install PlatformIO IDE VSCode extension. [Instructions](https://platformio.org/platformio-ide)
Install EditorConfig extension for your text editor. [Instructions](https://editorconfig.org/)
-Note: when making changes using the PlatformIO toolchain, please ensure the sketch still builds on Teensyduino before opening a PR.
+To open the project in VSCode :
+- open a new window
+- select the PlatformIO icon from the Primary Side Bar (left toolbar)
+- use the "Pick a folder" button to select the OMX-27 folder you created above
-## BOM
-
-[Bill of Materials]()
-
-## Build
-
-[Build Guide]()
-
-## Docs
-
-[Documentation]()
-
-## Web Configurator
-
-[Online Configurator](https://okyeron.github.io/OMX-27/webconfig/index.html)
## FAQ
Q: What key switches are recommended?
-A: Any RGB switches with a Cherry MX footprint can be used - I'm using Cherry MX RGB and these are linked in the [BOM](). Different varieties are available (Red, Brown, etc.)
+A: The board uses a dual-footprint for either Cherry MX or Kailh Choc V1 switches.
Q: Can I use other key switches?
-A: Yes - as long as they have the same footprint as Cherry MX switches and a window/opening for the LED to shine through. Low profile keys like the Cherry Low Profile or Kailh Choc switches have a different footprint and will not work.
+A: Yes - as long as they have the same footprint as Cherry MX or Kailh Choc V1 switches and a window/opening for the LED to shine through. NOTE - __Cherry Low Profile__ or__ Kailh Choc V2__ switches have a different footprint and will not work.
-Q: What about recommended Keycaps?
-A: Also listed in the [BOM](). You want an MX stem cap, with translucency or a window for the LED to shine through. DSA profile caps work well.
+Q: What about recommended Keycaps if I want to customize?
+A: Also listed in the [BOM](). It depends on which switches you use. You want a "shine thru" cap with a window for the LED.
Q: Does this project require soldering?
-A: Yes. Thru-hole soldering is required along with some easy SMD (LEDs and jacks).
-
-Q: What's with these LEDs?
-A: This project uses SK6812-MINI-E reverse mount LEDs. They are somewhat hard to find, so I'll try to offer them included with kits. They are easy to solder, even if you've not done much SMD.
+A: Yes. Thru-hole soldering is required (Pots and switches).
Q: Can I get the Gerbers or order the pcbs myself?
A: No. Not open source at this time.
diff --git a/build/BOM.md b/build/BOM.md
index e52ae2c6..65bf7fe2 100644
--- a/build/BOM.md
+++ b/build/BOM.md
@@ -1,76 +1,59 @@
-# OMX-27 BOM
+# OMX-27 v3 BOM
+OMX-27 v3 comes with SMD parts pre-assembled. Some alternate/replacement parts are listed here for reference
| Mouser | QTY | Part | Value | Package |
|-----|:--:|-----|-----|-----|
-|[RC0805FR-1047RL](http://www.mouser.com/Search/ProductDetail.aspx?R=RC0805FR-1047RL)|2|R7 R8|47R|0805|
-|[603-RC0805FR-0710KL](http://www.mouser.com/Search/ProductDetail.aspx?R=603-RC0805FR-0710KL)|2|R1 R2|10K|0805|
-|[652-CR0805-FX2202ELF](http://www.mouser.com/Search/ProductDetail.aspx?R=652-CR0805-FX2202ELF)|2|R4 R6|22K|0805|
-|[RC0805FR-1056KL](http://www.mouser.com/Search/ProductDetail.aspx?R=RC0805FR-1056KL)|2|R3 R5|56K|0805|
-|[80-C0805C104J5RACLR](http://www.mouser.com/Search/ProductDetail.aspx?R=80-C0805C104J5RACLR)|28|C1-C29|100nF|0805|
-|[710-885382207006](http://www.mouser.com/Search/ProductDetail.aspx?R=710-885382207006)|2|C30, C31|10nF|0805|
-|[621-1N4148W-F](http://www.mouser.com/Search/ProductDetail.aspx?R=621-1N4148W-F)|27|D1-D27|1N4148 Diode|SOD-123|
-|[SJ-3523-SMT-TR](http://www.mouser.com/Search/ProductDetail.aspx?R=SJ-3523-SMT-TR)|1|J1|SJ-3523-SMT-TR|3.5 mm jack stereo|
-|[490-MJ-3523-SMT-TR](http://www.mouser.com/Search/ProductDetail.aspx?R=490-MJ-3523-SMT-TR)|2|J2,J3|MJ-3523-SMT|3.5 mm jack mono|
-|[540-MX3A-L1NA](http://www.mouser.com/Search/ProductDetail.aspx?R=540-MX3A-L1NA)|27|K1-K27|CHERRY-MX|CHERRY-MX RGB Silent Red \*|
-|[595-TLV9062IDR](http://www.mouser.com/Search/ProductDetail.aspx?R=595-TLV9062IDR)|1|U1|SOIC127P600X175-8N|TLV9062IDR|
-|[AYZ0202AGRLC](http://www.mouser.com/Search/ProductDetail.aspx?R=AYZ0202AGRLC)|1|S1|DPDT Switch|SWITCH-DPDT-SMD-AYZ0202|
-|[688-RK09K1130A5R](http://www.mouser.com/Search/ProductDetail.aspx?R=688-RK09K1130A5R)|5|VR1-VR4,VR6|10K|9MM_SNAP-IN_POT*|
-|[652-PEC11R-4015F-S24](http://www.mouser.com/Search/ProductDetail.aspx?R=652-PEC11R-4015F-S24)|1|VR5|PEC11+SWITCH|Encoder with Switch|
-| [aliexpress](https://www.aliexpress.com/item/4000475685852.html?spm=a2g0s.9042311.0.0.601b4c4dcyhOZn) / [ebay](https://www.ebay.com/itm/100-2000pcs-SK6812-MINI-E-LED-CHIP-SK6812-3228-4pin-dream-color-LEDS-DC5V/224140435419?hash=item342fcf9fdb:g:XbAAAOSwzkRd8g96)|27|LED1-LED27|SK6812MINIE|SK6812-MINI-E|
-| [PJRC Store](https://www.pjrc.com/store/teensy32.html) |1| |TEENSY 3.2||
+|[688-RK09K1130A5R](http://www.mouser.com/Search/ProductDetail.aspx?R=688-RK09K1130A5R)|5|VR1-VR4,VR6|10K linear |9MM_SNAP-IN_POT*|
+|[652-PEC11R-4015F-S24](http://www.mouser.com/Search/ProductDetail.aspx?R=652-PEC11R-4015F-S24)|1|VR5|PEC11 + SWITCH|Encoder with Switch|
+|[540-MX3A-L1NA](http://www.mouser.com/Search/ProductDetail.aspx?R=540-MX3A-L1NA)|27|K1-K27|CHERRY-MX|CHERRY-MX RGB Silent Red \***|
+| (alt) |27|K1-K27|Kailh Choc V1|Kailh Choc V1 Red \***|
| |1| |OLED - 128x32 I2C display| \**See below|
-| | | |header pins| \***See below|
-\* POTS - I used trimmer type pots because they're a little more low profile. But you can use alpha pots or whatever you have around.
+\* POTS - I used trimmer type pots because they're a little more low profile. But you can use alpha pots (10K linear) or whatever you have around.
\** OLED - 128x32 I2C display (SSD1306) with pin order ( GND, VCC, SCL, SDA )
-example from eBay:
-"0.91" 128x32 IIC I2C White OLED LCD Display DIY Module For Arduino"
-https://www.ebay.com/itm/293660021494
+example search terms: "0.91" 128x32 IIC I2C White OLED LCD Display DIY Module For Arduino"
-\*** Headers:
-1X04 (oled)
-1x14 x 2 (teensy)
-1x01 (teensy dac pin)
+---
+### Optional battery
-TIP: Get 1x40 breakaway headers and cut what you need.
+A lipo battery can be added.
-[Mouser Cart (work in progress)](https://www.mouser.com/ProjectManager/ProjectDetail.aspx?AccessID=13c0107d30) - __DOES NOT include Teensy, OLED, LEDs or headers__
+[This battery from tinycircuits is recommended](https://tinycircuits.com/products/lithium-ion-polymer-battery-3-7v-290mah). Also available from [Mouser](https://www.mouser.com/ProductDetail/TinyCircuits/ASR00007) or [Digikey](https://www.digikey.com/en/products/detail/tinycircuits/ASR00007/7404517)
-Mounting Hardware is NOT LISTED
+The battery plug on the PCB is a JST SH connector (2 pin, 1.0 mm pitch) or you can solder the battery wires directly to the PCB. Please note the PCB markings for polarity.
-Knobs are up to you.
+A 3D printable battery box is also available. [Model link]
---
-### \* Key switches:
+### \*** Key switches:
+
+Cherry MX or Kailh Choc V1 switches can be used for the build. Denki-oto kits default to Cherry MX RGB Silent Red or Kailh Choc V1 Red switches and will have a different keyplate and spacer setup depending on the switch choice.
+
+Switches can be best obtained from various online keyboard shops like [mechanicalkeyboards.com](https://mechanicalkeyboards.com/).
-Any of the Cherry MX RGB switches will work. Red/SilentRed/Blue/Brown/Black/SpeedSilver.
-Red is linear (45g). Blue is clicky & tactile (50g). Brown is tactile (45g). Black is similar to Red but 60g actuation force. SpeedSilver (shorter key travel for gamers) are linear and __Expensive__( 45g)
-Cherry MX RGB part numbers:
+### Keycaps - Choc:
-| name | part num | type | actuation |
-|-----|----|-----|----|
-|SilentRed |[MX3A-L1NA](https://www.mouser.com/ProductDetail/CHERRY/MX3A-L1NA/?qs=F5EMLAvA7IA6PAS7ry3I9w%3D%3D)| linear | (45g) |
-|Red |MX1A-L1NA| linear | (45g) |
-|SilentBlack |[MX3A-11NA](https://www.mouser.com/ProductDetail/CHERRY/MX3A-11NA/?qs=F5EMLAvA7ICizK1XKjfN9w%3D%3D)| linear | (60g) |
-|Black |MX1A-11NA| linear | (60g) |
-|Brown |[MX1A-G1NA](https://www.mouser.com/ProductDetail/540-MX1A-G1NA/)| tactile | (45g) |
-|Blue |MX1A-E1NA| clicky & tactile | (50g) |
-|Silver |[MX1A-51NA](https://www.mouser.com/ProductDetail/CHERRY/MX1A-51NA/?qs=F5EMLAvA7IB4ByA0zXdBkg%3D%3D)| linear | (45g) |
+Choc Kits come with FK MBK "Dot Glow" keycaps.
-Reference: https://www.mouser.com/pdfDocs/cherrykeyswitches.pdf
+Nice blank Choc keycaps with shine-thru are hard to find. A translucent white MBK keycap made with POM material looks great with backlighting. Search online keyboard shops in your country for "MBK POM"
+FK offers a [Custom Service](https://fkcaps.com/custom/) if you wanted to create your own.
+
+---
-### Keycaps:
+### Keycaps - MX:
Any MX-compatible keycaps will work, but you'll want one designed for backlighting, such as a "backlit two-shot", "translucent", "shine-through", or "windowed".
I like the DSA profile caps for this application.
-[DSA "Dolch" Keyset (Two Shot) "Windowed" Keys](https://pimpmykeyboard.com/dsa-dolch-keyset-two-shot/) (choose the __LED Kit__ option).
-These come in a pack of 4 keycaps. You will need 7 packs (@ ~$70 total).
+I have the DSA windowed keycaps in various colors available for sale in sets [on my shop](https://www.denki-oto.com/store/p62/OMX-27_Keycap_sets.html#/).
+
+Signature Plastics sells the keycap I use in a Light or Dark grey - [DSA "Dolch" Keyset (Two Shot) "Windowed" Keys](https://spkeyboards.com/products/dsa-dolch) (choose the __LED Keys__ option).
+These come in a pack of 4 keycaps. You will need 7 packs (@ ~$60 total).
[Flashquark Translucent DSA Keycaps](https://flashquark.com/product/translucent-dsa-keycaps/) (in Black, White, Clear, Blue, Red) - __50 per pack__. ($12.99-$15.99 per pack)
@@ -79,6 +62,8 @@ These come in a pack of 4 keycaps. You will need 7 packs (@ ~$70 total).
[Maxkeyboard Black Translucent MX Blank](https://www.maxkeyboard.com/black-translucent-cherry-mx-blank-keycap-set-for-esc-w-a-s-d-or-e-s-d-f-and-arrow-keys.html) (pack of 9). ($21 for 3 packs)
+---
+
### Knobs
So many opinions about knobs.
@@ -88,9 +73,10 @@ https://www.thonk.co.uk/shop/tall-trimmer-toppers/
https://www.thonk.co.uk/shop/micro-knobs/
https://www.thonk.co.uk/shop/tall-trimmer-toppers/
+---
### USB Cable:
I recommend using a right angle extension cable [like this one from Amazon](https://www.amazon.com/gp/product/B015PSU5F6/)
-Be sure you have a good, known working, USB DATA CABLE and not just a charging cable.
+Be sure you have a good, known working, __USB DATA CABLE__ and not just a charging cable.
diff --git a/build/Build-Kit.md b/build/Build-Kit.md
index a3adb5a7..f3937b90 100644
--- a/build/Build-Kit.md
+++ b/build/Build-Kit.md
@@ -1,246 +1,123 @@
-# OMX-27
+# OMX-27 version 3
-
-
+### PLEASE READ THIS ENTIRE GUIDE FIRST
-# Before you start
+## Before you start
-## READ THIS ENTIRE GUIDE FIRST
+### Notes / Reminders
-Also - see these __Build Videos:__
+- Kit PCBs have been flashed and tested. They should be fully functional. The microprocessor, LEDs, OLED, etc. are already assembled. Thru-hole soldering is required for the pots, encoder and switches.
-[Part 1 - LEDs](https://youtu.be/UFm8Dfpjoz4)
-[Part 2 - Teensy](https://youtu.be/W-rJqxFzsLw) (Not yet updated for Teensy 4.0)
-[Part 3- Pots and Testing](https://youtu.be/rtUBW4xm9us)
-[Part 4 - Switches and Assembly](https://youtu.be/jUWWuaacoz4)
+- For the Cherry MX kit - the keyswitches are snapped into the keyplate first (before soldering them).
-The key-switches are going to be the VERY LAST thing you solder. __After you solder the switches in, everything on the inside is going to be inaccessible.__
+- For the Choc kits - the keyplate fits over the switches and it can be attached later.
-Ideally you want to be able to test all the LEDs, the OLED, and the pots/encoder before putting the switches on.
-
-I'd also suggest testing each switch connection with a piece of wire or tweezers so you can confirm the diodes/LEDs/caps are all soldered correctly.
-
-Follow the order of operations here to make your life easier. __NOTE - the keyswitches are absolutely the last thing you solder.__ Make sure everything else looks good before you do the switches.
-
-Also important - Keyswitches are snapped into the keyplate first (before soldering them).
-
-Don't forget to put the spacer layer in-between the main PCB and the keyplate before you solder all the switches.
+- Don't forget to put the spacer layer(s) in-between the main PCB and the keyplate before you solder all the switches.
### Soldering Tips
-I work with a fine point tip on my iron at 400C. With this setup I typically hold the iron on a pad for about 2 seconds and then apply a bit of solder and then hold the iron there for anything 2-3 seconds. You want to watch for the solder to flow around the joint, but not to hold the iron there forever.
+I work with a fine point or chisel tip on my iron at 380C-400C. With this setup I typically hold the iron on a pad for about 2 seconds and then apply a bit of solder and then hold the iron there for anything 2-3 seconds. You want to watch for the solder to flow around the joint, but not to hold the iron there forever.
See [Adafruit's guide to excellent soldering](https://learn.adafruit.com/adafruit-guide-excellent-soldering) for lots of good tips and tricks.
-Nice to have tools:
- - flush diagonal cutters
- - tweezers
-
-
----
-
-# Build from Kit
-
-### LEDs
-
-The LEDs are __Reverse Mount__ and are soldered to the back-side of the PCB with the LED facing towards the top of the PCB. When looking at the back of the PCB as in the picture, the GND leg is the top right pad for each one (marked with a red triangle in the picture below). The LED itself has a "notched" leg for GND.
-
-
-
-Set each LED into position (tweezers are handy for this) and __then double check the ground pin is in the right position__.
-
-
-
-
-
-Solder/tack the bottom right corner pad of each LED to hold each one in place. Then check the orientation of each LED to be sure they're nice and square in the hole. If not, warm up the solder there and reposition as needed.
-
-After you're happy with the LEDs being in the proper positions - solder the rest of the pads.
-
-### TEENSY
-
-For the keyplate to fit properly, the Teensy MUST be flush-mounted to the top of the main PCB.
-
-(Teensy 3.2 and PCB v1.5 only) An insulating kapton spacer is included with your kit . Use this between the bottom of the teensy and the main PCB to reduce the chances of unintended shorts.
-
-See below for Teensy 4.0 instructions (2023 v2.0 boards).
-
-__Teensy 3.2 jig__
-
-Use the included acrylic jig to set up your Teensy like the following for soldering.
-
-Short side of the headers goes down to the jig and the long side up.
-
-
-
-Add a 1x3 and 1x1 in the appropriate places. The 1x1 directly next to the 1x3 is not connected to anything so you can solder that or not (your choice).
-
-
-
-
-Add the two spacers (maybe even tape those two together so they don't wiggle around.
-
-
-
-Drop the Teensy into place. There should just be a small amount of header sticking up from the Teensy at this point.
-
-
-
-DON'T SOLDER A HEADER TO THE VUSB PIN - it's not used. This is the 1x1 pin/hole right next to the USB jack on the Teensy (on the inside row).
-
-__Teensy 4.0 jig__
-
-The Teensy 4.0 version (board v2.0) only uses the 2 outer rows of pins. (4 less pins to solder!)
-
-Note the plastic parts of the jig have an etched out area - this is to allow space for the components on the underside of the Teensy 4.0.
-
-
-
-
-__Soldering__
-
-Solder the pins to the Teensy first.
-Then remove the jig and carefully remove the black plastic from the headers. __Hold onto the black spacers for the next step.__
-After you've removed the plastic, slide the thin yellow kapton spacer thingy onto the bottom of the teensy - this should end up between the teensy and the main board as an insulator. Then drop the Teensy onto the main board so it sits nice and flat.
+# Build Video
-
+[Build/assembly](https://youtu.be/jUWWuaacoz4)
-To keep the pins from wiggling around while soldering the bottom, either
-
- * Put a big piece of tape over the whole teensy to keep it in place and to keep the pins from getting pushed out
-
- * Or push the black plastic bits from the headers onto the pins to hold them in place while soldering
-
- * Or both
-
-
-
-Flip the board over and solder the pins to the bottom. Try to tack/solder one pin on either side in place while pushing your finger against the teensy to make sure it's absolutely flat against the main pcb.
-
-Once you're happy with the flatness - solder the rest of the pins. Be careful not to push down on the pins while soldering.
-
-Using flush cutters, trim the pins away. Be careful not to nick/scratch the pcb.
-
-
-
-### OLED
-
-The OLED display sits on a regular header (not flush like the Teensy) the display should be close to level with the keyplate (the OLED glass will be about 0.5-1mm higher than the keyplate).
-
-__TIP:__ I suggest using a section of the header plastic you removed from the Teensy headers as a spacer to hold up the other side of the OLED PCB. Glue or tape a 1x4 chunk of the header plastic to the back of the OLED pcb and this will keep it level and support it while you solder (and after).
+---
-
-
+# Kit Build Instructions
-Trim the headers on the top side of the OLED if you're worried about something shorting there.
+The microprocessor, LEDs, OLED, etc. are already assembled. Thru-hole soldering is required for the pots, encoder and switches.
-### JACKS, POTS, ENCODER, ETC.
+## POTS AND ENCODER
Snap pots and encoders into place and solder.
You may need to gently squeeze the snap-in mounting pins together a tiny bit to get the pots to snap into place.
+Note - You don't need loads of solder on the lugs - just enough to keep them in place.
+

---
-# __STOP HERE AND TEST THINGS__
-
-At this point you can flash the firmware and do some testing.
-
-See the instructions here (loading a HEX file) - https://llllllll.co/t/how-to-flash-the-firmware-on-a-teensy-micro-controller/20317 if you don't know how to flash firmware to a Teensy.
-
-The OLED should display something as soon as you plug into USB power.
-
-### LED test
-
-On startup all the LEDs should show a rainbow pattern.
-
-If your LEDs work up to a certain point (e.g. LEDs 1-7 work, LED 8-27 don't):
-
-- The problem is most likely a bad soldering joint on the erroneous LED itself, or on the LED that is RIGHT BEFORE this LED in the chain (in the above example, check LED 7 and 8). Carefully re-solder all connections again to fix the problem (melt the existing solder again, maybe apply some more, make sure it flows nicely between LED and PCB pad)
+## STOP HERE AND TEST
-- Check that the orientation of the LED is correct (see pictures above)
+At this point you can do a quick test.
-
+Plug into USB power and the OLED should display the startup routine. Then the LEDs will light up in a "rainbow" pattern and the main screen will show on the display. Turn the encoder to the right to get to the second page and you can test each of the pots - which should show the CC number and value on the display.
-### Switch contact test - AKA "the tweezer test"
+If you get random values for the CCs, you may need to check and reflow the solder on the potentiometer pins.
-You will want to test the pads for each keyswitch on the PCB using tweezers or a piece of wire (a piece of wire will work much better than tweezers!). This is also a second check that the LED for that switch is working correctly.
+If the display does not light up or you don't see LEDs light up when you connect to USB power, double check the Power Switch on the right edge of the device. The "On" position is towards the USB port.
-
+---
-When you test the AUX key (top left-most key) - this will light up a total of 15 LEDs on the board. This is normal.
+# Continue building
-
+## Case Parts
-If the LEDs do not light up for each switch contact, check the LEDs again first. A good test is to remove power and re-plug to see if the rainbow LED pattern shows on startup. If all the LEDs are working OK examine the diode adjacent to that switch position and be sure the soldering looks OK.
+__Carefully__ remove the paper backing from the acrylic base plate and set it aside.
-Note - There are groups of Rows and Columns for sets of switches. If you get a group lighting up, it may be a corresponding pin on the Teensy for that row or column. Ask on Discord if you're stuck here.
+If the brown paper backing material does not come off easily, you can often rub your finger along the surface to get the paper backing to "roll up" a bit
+Cherry MX kits have __two__ foam spacer layers. The Choc kit has __one__ foam spacer layer.
-### MIDI test
+For the Cherry MX kit, you will need the PCB "keyplate" for the next step. For the Choc kit, set the keyplate aside until later.
-Use the [browser_test](../browser_test/index.html) script to show USB-MIDI input to your computer. Then you can check to be sure the pots are sending CCs and that you get MIDI note-ons/note-offs when you test each keyswitch's pads. Be sure you have the `oct` (octave) set to 4 on the display (change with encoder knob).
-Also test the Hardware MIDI 1/8" jack with an appropriate adapter and synth. Check the A/B switch position for your particular setup (try both to be sure you have the right one).
+## Key Switch Prep (MX Kit)
----
+Check the orientation of the switches. The pins go towards the bottom-half and the LED window is at the top.
-# Continue building
+Snap all the key-switches into the keyplate (from the top - keyplate top has text labels for the MIDI anc CV jacks).
-### Acrylic Case Parts
+The switches may be a tight fit in the keyplate. Be sure they are snapped all the way into place.
-__Carefully__ remove the paper backing from the acrylic parts - the spacer and the back plate. Then set these aside for the next step.
+Set the two black foam spacer layers on the main PCB and align it around the various components. Then set the keyplate with switches into place to be sure all the pins line up and everything is nice and flat.
-The spacer layer is pretty fragile - try not to break it. However, even if it does break, it might be fine since this sits in-between the other layers.
+Inspect from the bottom that you have 2 pins sticking out for each switch. You may need to gently bend key-switch pins into place if they got slightly bent in transport.
+Use the three of included case screws/nuts to fix everything together for soldering. I suggest using the holes down the middle of the case. This will ensure the key switches are held in place for soldering and that everything will remain flat.
-### Key Switches
+## Key Switch Prep (Choc Kit)
Check the orientation of the switches. The pins go towards the bottom-half and the LED window at the top.
-
-
-Snap all the key-switches into the keyplate (from the top).
+Insert all the key-switches into the PCB.
-
+## Switch Soldering
-The switches may be a tight fit. Be sure they are snapped all the way into place.
+I suggest going thru and tack-soldering one pin for each switch (just enough solder to hold it in place). Then for each switch - use your not-soldering-iron hand to fully press each switch with your thumb on the bottom side, and re-heat the tacked solder joint. In some cases the swtich might not have been fully inserted and it will snap into place when you do this.
-Set the black acrylic spacer layer on the main PCB and align it around the various components. Then set the keyplate with switches into place to be sure all the pins line up and everything is nice and flat. You may need to gently bend key-switch pins into place if they got slightly bent in transport.
+After all the switches look nice, make another pass and solder the other pin for each switch. Make a third pass and add a touch of solder if needed to the pins you tacked in place earlier.
-
-
-Use the included case screws/nuts to fix everything together for soldering. I suggest using the holes down the middle of the case. This will ensure the key switches are held in place for soldering and that everything will remain flat.
+## Assembly
-
-
-Solder all the switches.
-
-### Bottom Plate
-
-Then remove the screws/nuts and then reassemble with the bottom plate.
+Remove the screws/nuts if you used them earlier and then reassemble with the bottom plate.
The nuts fit into the captive cutouts on the bottom plate.
+There are three longer screws which are used for the USB protector.
+


-### Teensy Cover
+### USB Protector/Cover
-Add the teensy cover plate with the two remaining screws/nuts.
+Add the cover plate with the three remaining (longer) screws/nuts.

-### Pot Knobs
+### Knobs
-Push the knobs onto the pots, make sure the marking on the knob aligns with the marking on the pot.
+Push the knobs onto the pots, make sure the marking on the knob aligns with the marking on the pot. Be carefull not to press the knobs on too hard if you want to change them later. They can be difficult to remove if pressed on too hard.

@@ -249,3 +126,14 @@ Push the knobs onto the pots, make sure the marking on the knob aligns with the
Then install the keycaps with the window on the top for the LEDs.

+
+
+
+
+
+## MIDI test
+
+You can use the [browser_test](../browser_test/index.html) script to show USB-MIDI input to your computer. Then you can check to be sure the pots are sending CCs and that you get MIDI note-ons/note-offs when you test each keyswitch's pads. Be sure you have the `oct` (octave) set to 4 on the display (change with encoder knob).
+
+Also test the Hardware MIDI OUT 1/8" jack (with an appropriate adapter if needed) and a synth of your choice. Check the MIDI A/B switch position for your particular setup (try both to be sure you have the right one).
+
diff --git a/images/808caps.png b/images/808caps.png
new file mode 100644
index 00000000..f402bc2a
Binary files /dev/null and b/images/808caps.png differ
diff --git a/images/DSC06421.png b/images/DSC06421.png
new file mode 100644
index 00000000..da1641e3
Binary files /dev/null and b/images/DSC06421.png differ
diff --git a/images/DSC06422.jpg b/images/DSC06422.jpg
new file mode 100644
index 00000000..07154d2a
Binary files /dev/null and b/images/DSC06422.jpg differ
diff --git a/images/DSC06424.jpg b/images/DSC06424.jpg
new file mode 100644
index 00000000..7bf2ed95
Binary files /dev/null and b/images/DSC06424.jpg differ
diff --git a/images/DSC06427.jpg b/images/DSC06427.jpg
new file mode 100644
index 00000000..6bba7402
Binary files /dev/null and b/images/DSC06427.jpg differ
diff --git a/images/DSC06429.jpg b/images/DSC06429.jpg
new file mode 100644
index 00000000..e2fdebe7
Binary files /dev/null and b/images/DSC06429.jpg differ
diff --git a/images/DSC06433.png b/images/DSC06433.png
new file mode 100644
index 00000000..8f22a6f6
Binary files /dev/null and b/images/DSC06433.png differ
diff --git a/images/boot-reset.png b/images/boot-reset.png
new file mode 100644
index 00000000..61e84064
Binary files /dev/null and b/images/boot-reset.png differ
diff --git a/platformio.ini b/platformio.ini
index 9b8944a4..98a0a8ec 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -10,33 +10,32 @@
[platformio]
src_dir = OMX-27-firmware
+include_dir =
-[env]
+
+[env:pico]
+platform = https://github.com/maxgerhardt/platform-raspberrypi.git
+board = pico
framework = arduino
-upload_protocol = teensy-cli
+board_build.core = earlephilhower
+board_build.filesystem_size = 1m
+build_flags =
+ -DUSE_TINYUSB
monitor_speed = 115200
-lib_deps =
+
+lib_deps =
+ adafruit/Adafruit TinyUSB Library@^3.3.4
+ fortyseveneffects/MIDI Library @ ^5.0.2
adafruit/Adafruit BusIO @ ^1.9.3
adafruit/Adafruit FRAM I2C @ ^2.0.0
adafruit/Adafruit Keypad @ ^1.3.0
- adafruit/Adafruit NeoPixel @ ^1.9.0
+ adafruit/Adafruit NeoPixel @ ^1.12.3
adafruit/Adafruit SSD1306 @ ^2.4.6
adafruit/Adafruit GFX Library @ ^1.10.12
olikraus/U8g2_for_Adafruit_GFX @ ^1.8.0
adafruit/Adafruit MCP4725@^2.0.0
-
-extra_scripts = clear_storage/register_storage_target.py
-
-[env:teensy40]
-platform = teensy
-board = teensy40
-build_flags = -D USB_MIDI_SERIAL
-
-[env:teensy31]
-platform = teensy
-board = teensy31
-; build_flags = -D USB_MIDI_SERIAL -D TEENSY_OPT_FAST_LTO
-build_flags = -D USB_MIDI_SERIAL -D TEENSY_OPT_SMALLEST_CODE_LTO
-; build_flags = -D USB_MIDI_SERIAL -D TEENSY_OPT_DEBUG_LTO
-
-
+ dxinteractive/ResponsiveAnalogRead @ ^1.2.1
+ stechio/Analog-Digital Multiplexers@^3.0.0
+ pfeerick/elapsedMillis@^1.0.6
+
+; extra_scripts = clear_storage/register_storage_target.py