├── .gitignore ├── LICENSE ├── README.md ├── app.py ├── ddrm_tone_selector_mode.py ├── definitions.py ├── display_utils.py ├── docs ├── diagram.jpeg ├── diagram.key ├── pysha_instrument_selection_midi_cc.jpeg ├── pysha_melodic_mode.jpeg ├── pysha_rhythmic_mode.jpeg └── pysha_velocity_curves.jpeg ├── instrument_definitions ├── DDRM.json ├── DOMINION.json ├── KIJIMI.json ├── MINITAUR.json ├── OCTATRACK.json └── SOURCE.json ├── main_controls_mode.py ├── melodic_mode.py ├── midi_cc_mode.py ├── preset_selection_mode.py ├── pyramid_track_triggering_mode.py ├── requirements.txt ├── rhythmic_mode.py ├── settings_mode.py ├── slice_notes_mode.py ├── track_listing.json └── track_selection_mode.py /.gitignore: -------------------------------------------------------------------------------- 1 | settings.json 2 | favourite_presets.json 3 | .DS_Store 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | .vscode/ 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | pip-wheel-metadata/ 30 | share/python-wheels/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | MANIFEST 35 | 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .nox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *.cover 56 | *.py,cover 57 | .hypothesis/ 58 | .pytest_cache/ 59 | 60 | # Translations 61 | *.mo 62 | *.pot 63 | 64 | # Django stuff: 65 | *.log 66 | local_settings.py 67 | db.sqlite3 68 | db.sqlite3-journal 69 | 70 | # Flask stuff: 71 | instance/ 72 | .webassets-cache 73 | 74 | # Scrapy stuff: 75 | .scrapy 76 | 77 | # Sphinx documentation 78 | docs/_build/ 79 | 80 | # PyBuilder 81 | target/ 82 | 83 | # Jupyter Notebook 84 | .ipynb_checkpoints 85 | 86 | # IPython 87 | profile_default/ 88 | ipython_config.py 89 | 90 | # pyenv 91 | .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 101 | __pypackages__/ 102 | 103 | # Celery stuff 104 | celerybeat-schedule 105 | celerybeat.pid 106 | 107 | # SageMath parsed files 108 | *.sage.py 109 | 110 | # Environments 111 | .env 112 | .venv 113 | env/ 114 | venv/ 115 | ENV/ 116 | env.bak/ 117 | venv.bak/ 118 | 119 | # Spyder project settings 120 | .spyderproject 121 | .spyproject 122 | 123 | # Rope project settings 124 | .ropeproject 125 | 126 | # mkdocs documentation 127 | /site 128 | 129 | # mypy 130 | .mypy_cache/ 131 | .dmypy.json 132 | dmypy.json 133 | 134 | # Pyre type checker 135 | .pyre/ 136 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Frederic Font 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pysha 2 | 3 | **Pysha** is a Python 3 app to use **Push2 as a standalone MIDI controller**. It has manily been designed to work as a controller for [Squarp's Pyramid](https://squarp.net/pyramid/), but it can also be used as a generic controller. To run Pysha, you just need to install Python requirements and run `app.py` on a computer connected to Push2 and with a MIDI interface to output messages. 4 | 5 | ``` 6 | pip install -r requirements.txt 7 | python app.py 8 | ``` 9 | 10 | Pysha **can run on a Raspberry Pi** (see instructions below) so you can use Push2 as a standalone controller without your laptop around. Pysha is based on [push2-python](https://github.com/ffont/push2-python). `push2-python` requires [pyusb](https://github.com/pyusb/pyusb) which is based in [libusb](https://libusb.info/). You'll most probably need to manually install `libusb` for your operative system if `pip install -r requirements.txt` does not do it for you. Moreover, to draw on Push2's screen, Pysha uses [`pycairo`](https://github.com/pygobject/pycairo) Python package. You'll most probably also need to install [`cairo`](https://www.cairographics.org/) if `pip install -r requirements.txt` does not do it for you (see [this page](https://pycairo.readthedocs.io/en/latest/getting_started.html) for info on that). The name "Pysha" is some sort of blend of the names of the technologies/devices that are used. 11 | 12 | 13 | **NOTE**: Development for Pysha as a controller for Squarp's Pyramid has been abandoned and has evolved into [Shepherd](http://github.com/ffont/shepherd), a full "Raspberry Pi + Push 2" based MIDI sequencer which does not depend on any external piece of hardware. 14 | 15 | 16 | ## Features 17 | 18 | I designed Pysha (and I continue to update it) to serve my own specific setup needs, but hopefully it can be useful (or adapted!) to work on other setups as well. In my setup, I run Pysha on a Rapsberry Pi and connected to Push2. Push2 is used as my main source of MIDI input, and the generated MIDI is routed to a Squarp Pyramid sequencer. From there, Pyramid connects to all the other machines in the setup. Also, I have a MIDI keyboard connected to Pysha so that the notes generated from the keyboard are merges with the notes generated from Push. Below is a diagram of my setup with Pysha. These are the features that Pysha has currently implemented: 19 | 20 | * Play melodies and chords in a chromatic scale mode 21 | * Use classic 4x4 (and up to 8x8!) pad grid in the rhythm layout mode 22 | * Choose between channel aftertouch and polyphonic aftertouch (note: unfortunately polyphonic aftertouch mode won't work with Pyramid) 23 | * Use *accent* mode for fixed 127 velocity playing 24 | * Use touchstrip as a pitch bend or modulation wheel 25 | * Interactively adjust velocity/aftertouch sensitivity curves 26 | * Merge MIDI in from a MIDI input (using a MIDI intergace with the Rapsberry Pi) and also send it to the main MIDI out 27 | * Interactively configure MIDI in/out settings 28 | * Select Pyramid tracks and show track number information on screen 29 | * Show track instrument information and sync colors (with preloaded information about what each Pyramid track is routed to) 30 | * Mute/unmute 64 Pyramid tracks displayed in Push2's 64 pads 31 | * Send MIDI control CC data using the encoders, use synth definition files (much like Pyramid's) to show show controls and control names in a meaningful way 32 | * Select track instrument presets by sending program change messages 33 | * Temporarily disable screen rendering for slow Raspberry Pi's (like mine!) 34 | * Save current settings so these are automatically loaded on next run 35 | * Easy software update (provided an internet connection is working) 36 | 37 | 38 | 39 | 40 | 41 | Here are a couple of photos of Pysha working: 42 | 43 | 44 | 45 | 46 | *Melodic mode* 47 | 48 | 49 | 50 | *Rhythmic mode* 51 | 52 | 53 | 54 | *MIDI CC controls with instrument definition file* 55 | 56 | 57 | 58 | *Interactive adjustment of aftertouch range/velocity curve* 59 | 60 | 61 | ## User manual 62 | 63 | Well, this is not a proper user manual, but here are some notes about how to use Pysha: 64 | 65 | * Press `Note` button to toggle between rhythmic/melodic layouts. 66 | * Use `Ocateve up` and `Octave down` buttons to change octaves. 67 | * Press `Shift` button to toggle between pitch bend/modulation wheel modes for the touchstrip. 68 | * Press `Accent` button to activate fixed velocity mode (all notes will be triggered with full 127 velocity). 69 | * Press `Setup` button several times to cycle through configuration pages where you'll find options to: 70 | * Set MIDI out device and channel 71 | * Set MIDI in device and channel (for MIDI merge functionality) 72 | * Set Pyramidi MIDI channel 73 | * Set MIDI root note 74 | * Toggle between polyphonic/channel aftertouch modes 75 | * Configure channel pressure range and velocity/polyphonic aftertouch pad response curves 76 | * Save current settings to file (will be loaded automatically when Pysha runs again) 77 | * Run software update (to update Pysha version, requires internet connection) 78 | * Reset Push MIDI configuration (sometimes this is needed if not all pads are lit as expected or you see wrong button colors) 79 | * Select Pyramid tracks 1-8 by pressing the 8 buttons right above the pads. 80 | * Select Pyramid tracks 1-64 by holding one of the 8 buttons right above the pads and then pressing one of the 8 buttons to the right of pads (i.e. `1/32t`, `1/32`...). 81 | * Send MIDI CC messages using the 8 encoders above the display. The display will show feedback about which CC values are being sent. 82 | * Navigate between groups of CC controls using the 8 buttons above the display, and the `Page left`/`Page right` buttons. 83 | * Use instrument definition files to show proper MIDI CC control names and group them in meaningful sections. See examples in the `instrument_definitions` folder. 84 | * Customize Pyramid track contents editing the `track_listing.json` file. What comes by default is what I use in my setup. 85 | * Press `User` button to deactivate the display (useful for slow computers running Pysha). 86 | * Press `Add track` button to enter *Pyramid track triggering* mode (or hold the button to only momentarily activate that mode). While in this mode, you can mute/unmute the 64 Pyramid tracks using the 64 pads of the Push. Note that Pysha does not get information from Pyramid about the current status of tracks, therefore it might be out of sync with it. You can manually indicate that a track "has content" by pressing the corresponding pad, then you can mute/unmute that track by pressing the pad again. Long pressing one pad will set the corresponding track to "no content" state. In this way, you can manualy sync the track status in Pysha and the track status from Pyramid. Hopefully future Pyramid updated will allow to do this process automatically and provide tighter integration. 87 | * While in *Pyramid track triggering*, use the 8 buttons on the right of pads (i.e. `1/32t`, `1/32`...) to trigger unmute of all the tracks in the selected row (that have content), and mute all other tracks. This enables a scene triggering workflow similar to that of Ableton Live. 88 | * Also while in *Pyramid track triggering*, hold `Master` button and press one of the track pads to select that track (and exit the track triggering mode). 89 | * Press `Add device` button to enter *Preset selection mode* (or hold the button to only momentarily activate that mode). While in this mode, press any of the 64 pads to send a program change message to the corresponding Pyramid track synth with values 0-63. This allows you to select one of the first 64 presets for the current bank. Long-press one of the pads to mark this preset as "favourite" and highlight it (this info is saved). Long-press again to "unfavorite" the preset. Use left and right arrows to move to the next 64 presets (64-127) and iterate through available banks. 90 | 91 | 92 | ## Instructions to get Pysha running on a RaspberryPi 93 | 94 | These are instructions to have the script running on a Rapsberry Pi and load at startup. I'm using this with a Raspberry Pi 2 and Raspbian 2020-02-13. It works a bit slow but it works. I also tested on a Raspberry Pi 4 and it is much faster and reliable. 95 | 96 | These instructions assume you have ssh connection with the Rpasberry Pi. Here are the instructions for [enabling ssh](https://www.raspberrypi.org/documentation/remote-access/ssh/) on the Pi. Here are instructions for [setting up wifi networks](https://www.raspberrypi.org/documentation/configuration/wireless/wireless-cli.md). Also, here are instructions for [changing the hostname](https://thepihut.com/blogs/raspberry-pi-tutorials/19668676-renaming-your-raspberry-pi-the-hostname) of the Pi so for example you can access it like `ssh pi@pysha`. 97 | 98 | 1. Install system dependencies 99 | ``` 100 | sudo apt-get update && sudo apt-get install -y libusb-1.0-0-dev libcairo2-dev python3 python3-pip git libasound2-dev libatlas-base-dev 101 | ``` 102 | 103 | 2. Clone the app repository 104 | ``` 105 | git clone https://github.com/ffont/pysha.git 106 | ``` 107 | 108 | 3. Install Python dependencies 109 | ``` 110 | cd pysha 111 | pip3 install -r requirements.txt 112 | ``` 113 | 114 | 4. Configure permissions for using libusb without sudo (untested with these specific commands, but should work) 115 | 116 | Create a file in `/etc/udev/rules.d/50-push2.rules`... 117 | 118 | sudo nano /etc/udev/rules.d/50-push2.rules 119 | 120 | ...with these contents: 121 | 122 | add file contents: SUBSYSTEM=="usb", ATTR{idVendor}=="2982", ATTR{idProduct}=="1967", GROUP="audio" 123 | 124 | Then run: 125 | 126 | sudo udevadm control --reload-rules 127 | sudo udevadm trigger 128 | 129 | 130 | 5. Configure Python script to run at startup: 131 | 132 | Create file in `/lib/systemd/system/pysha.service`... 133 | 134 | sudo nano /lib/systemd/system/pysha.service 135 | 136 | ...with these contents: 137 | 138 | ``` 139 | [Unit] 140 | Description=Pysha 141 | After=network-online.target 142 | 143 | [Service] 144 | WorkingDirectory=/home/pi/pysha 145 | ExecStart=/usr/bin/python3 /home/pi/pysha/app.py 146 | StandardOutput=syslog 147 | User=pi 148 | Restart=always 149 | RestartSec=3 150 | 151 | [Install] 152 | WantedBy=multi-user.target 153 | ``` 154 | 155 | Set permissions to file: 156 | 157 | sudo chmod 644 /lib/systemd/system/pysha.service 158 | 159 | 160 | Enable the service (and do the linger thing which really I'm not sure if it is necessary nor what it does) 161 | 162 | loginctl enable-linger pi 163 | sudo systemctl enable pysha.service 164 | 165 | After that, the app should at startup. You can start/stop/restart and check logs running: 166 | 167 | sudo systemctl start|stop|restart pysha 168 | sudo journalctl -fu pysha 169 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import platform 4 | import time 5 | import traceback 6 | 7 | import cairo 8 | import definitions 9 | import mido 10 | import numpy 11 | import push2_python 12 | 13 | from melodic_mode import MelodicMode 14 | from track_selection_mode import TrackSelectionMode 15 | from pyramid_track_triggering_mode import PyramidTrackTriggeringMode 16 | from rhythmic_mode import RhythmicMode 17 | from slice_notes_mode import SliceNotesMode 18 | from settings_mode import SettingsMode 19 | from main_controls_mode import MainControlsMode 20 | from midi_cc_mode import MIDICCMode 21 | from preset_selection_mode import PresetSelectionMode 22 | from ddrm_tone_selector_mode import DDRMToneSelectorMode 23 | 24 | from display_utils import show_notification 25 | 26 | 27 | class PyshaApp(object): 28 | 29 | # midi 30 | midi_out = None 31 | available_midi_out_device_names = [] 32 | midi_out_channel = 0 # 0-15 33 | midi_out_tmp_device_idx = None # This is to store device names while rotating encoders 34 | 35 | midi_in = None 36 | available_midi_in_device_names = [] 37 | midi_in_channel = 0 # 0-15 38 | midi_in_tmp_device_idx = None # This is to store device names while rotating encoders 39 | 40 | notes_midi_in = None # MIDI input device only used to receive note messages and illuminate pads/keys 41 | notes_midi_in_tmp_device_idx = None # This is to store device names while rotating encoders 42 | 43 | # push 44 | push = None 45 | use_push2_display = None 46 | target_frame_rate = None 47 | 48 | # frame rate measurements 49 | actual_frame_rate = 0 50 | current_frame_rate_measurement = 0 51 | current_frame_rate_measurement_second = 0 52 | 53 | # other state vars 54 | active_modes = [] 55 | previously_active_mode_for_xor_group = {} 56 | pads_need_update = True 57 | buttons_need_update = True 58 | 59 | # notifications 60 | notification_text = None 61 | notification_time = 0 62 | 63 | # fixing issue with 2 lumis and alternating channel pressure values 64 | last_cp_value_recevied = 0 65 | last_cp_value_recevied_time = 0 66 | 67 | def __init__(self): 68 | if os.path.exists('settings.json'): 69 | settings = json.load(open('settings.json')) 70 | else: 71 | settings = {} 72 | 73 | self.set_midi_in_channel(settings.get('midi_in_default_channel', 0)) 74 | self.set_midi_out_channel(settings.get('midi_out_default_channel', 0)) 75 | self.target_frame_rate = settings.get('target_frame_rate', 60) 76 | self.use_push2_display = settings.get('use_push2_display', True) 77 | 78 | self.init_midi_in(device_name=settings.get('default_midi_in_device_name', None)) 79 | self.init_midi_out(device_name=settings.get('default_midi_out_device_name', None)) 80 | self.init_notes_midi_in(device_name=settings.get('default_notes_midi_in_device_name', None)) 81 | self.init_push() 82 | 83 | self.init_modes(settings) 84 | 85 | def init_modes(self, settings): 86 | self.main_controls_mode = MainControlsMode(self, settings=settings) 87 | self.active_modes.append(self.main_controls_mode) 88 | 89 | self.melodic_mode = MelodicMode(self, settings=settings) 90 | self.rhyhtmic_mode = RhythmicMode(self, settings=settings) 91 | self.slice_notes_mode = SliceNotesMode(self, settings=settings) 92 | self.set_melodic_mode() 93 | 94 | self.track_selection_mode = TrackSelectionMode(self, settings=settings) 95 | self.pyramid_track_triggering_mode = PyramidTrackTriggeringMode(self, settings=settings) 96 | self.preset_selection_mode = PresetSelectionMode(self, settings=settings) 97 | self.midi_cc_mode = MIDICCMode(self, settings=settings) # Must be initialized after track selection mode so it gets info about loaded tracks 98 | self.active_modes += [self.track_selection_mode, self.midi_cc_mode] 99 | self.track_selection_mode.select_track(self.track_selection_mode.selected_track) 100 | self.ddrm_tone_selector_mode = DDRMToneSelectorMode(self, settings=settings) 101 | 102 | self.settings_mode = SettingsMode(self, settings=settings) 103 | 104 | def get_all_modes(self): 105 | return [getattr(self, element) for element in vars(self) if isinstance(getattr(self, element), definitions.PyshaMode)] 106 | 107 | def is_mode_active(self, mode): 108 | return mode in self.active_modes 109 | 110 | def toggle_and_rotate_settings_mode(self): 111 | if self.is_mode_active(self.settings_mode): 112 | rotation_finished = self.settings_mode.move_to_next_page() 113 | if rotation_finished: 114 | self.active_modes = [mode for mode in self.active_modes if mode != self.settings_mode] 115 | self.settings_mode.deactivate() 116 | else: 117 | self.active_modes.append(self.settings_mode) 118 | self.settings_mode.activate() 119 | 120 | def toggle_ddrm_tone_selector_mode(self): 121 | if self.is_mode_active(self.ddrm_tone_selector_mode): 122 | # Deactivate (replace ddrm tone selector mode by midi cc and track selection mode) 123 | new_active_modes = [] 124 | for mode in self.active_modes: 125 | if mode != self.ddrm_tone_selector_mode: 126 | new_active_modes.append(mode) 127 | else: 128 | new_active_modes.append(self.track_selection_mode) 129 | new_active_modes.append(self.midi_cc_mode) 130 | self.active_modes = new_active_modes 131 | self.ddrm_tone_selector_mode.deactivate() 132 | self.midi_cc_mode.activate() 133 | self.track_selection_mode.activate() 134 | else: 135 | # Activate (replace midi cc and track selection mode by ddrm tone selector mode) 136 | new_active_modes = [] 137 | for mode in self.active_modes: 138 | if mode != self.track_selection_mode and mode != self.midi_cc_mode: 139 | new_active_modes.append(mode) 140 | elif mode == self.midi_cc_mode: 141 | new_active_modes.append(self.ddrm_tone_selector_mode) 142 | self.active_modes = new_active_modes 143 | self.midi_cc_mode.deactivate() 144 | self.track_selection_mode.deactivate() 145 | self.ddrm_tone_selector_mode.activate() 146 | 147 | def set_mode_for_xor_group(self, mode_to_set): 148 | '''This activates the mode_to_set, but makes sure that if any other modes are currently activated 149 | for the same xor_group, these other modes get deactivated. This also stores a reference to the 150 | latest active mode for xor_group, so once a mode gets unset, the previously active one can be 151 | automatically set''' 152 | 153 | if not self.is_mode_active(mode_to_set): 154 | 155 | # First deactivate all existing modes for that xor group 156 | new_active_modes = [] 157 | for mode in self.active_modes: 158 | if mode.xor_group is not None and mode.xor_group == mode_to_set.xor_group: 159 | mode.deactivate() 160 | self.previously_active_mode_for_xor_group[mode.xor_group] = mode # Store last mode that was active for the group 161 | else: 162 | new_active_modes.append(mode) 163 | self.active_modes = new_active_modes 164 | 165 | # Now add the mode to set to the active modes list and activate it 166 | new_active_modes.append(mode_to_set) 167 | mode_to_set.activate() 168 | 169 | def unset_mode_for_xor_group(self, mode_to_unset): 170 | '''This deactivates the mode_to_unset and reactivates the previous mode that was active for this xor_group. 171 | This allows to make sure that one (and onyl one) mode will be always active for a given xor_group. 172 | ''' 173 | if self.is_mode_active(mode_to_unset): 174 | 175 | # Deactivate the mode to unset 176 | self.active_modes = [mode for mode in self.active_modes if mode != mode_to_unset] 177 | mode_to_unset.deactivate() 178 | 179 | # Activate the previous mode that was activated for the same xor_group. If none listed, activate a default one 180 | previous_mode = self.previously_active_mode_for_xor_group.get(mode_to_unset.xor_group, None) 181 | if previous_mode is not None: 182 | del self.previously_active_mode_for_xor_group[mode_to_unset.xor_group] 183 | self.set_mode_for_xor_group(previous_mode) 184 | else: 185 | # Enable default 186 | # TODO: here we hardcoded the default mode for a specific xor_group, I should clean this a little bit in the future... 187 | if mode_to_unset.xor_group == 'pads': 188 | self.set_mode_for_xor_group(self.melodic_mode) 189 | 190 | def toggle_melodic_rhythmic_slice_modes(self): 191 | if self.is_mode_active(self.melodic_mode): 192 | self.set_rhythmic_mode() 193 | elif self.is_mode_active(self.rhyhtmic_mode): 194 | self.set_slice_notes_mode() 195 | elif self.is_mode_active(self.slice_notes_mode): 196 | self.set_melodic_mode() 197 | else: 198 | # If none of melodic or rhythmic or slice modes were active, enable melodic by default 199 | self.set_melodic_mode() 200 | 201 | def set_melodic_mode(self): 202 | self.set_mode_for_xor_group(self.melodic_mode) 203 | 204 | def set_rhythmic_mode(self): 205 | self.set_mode_for_xor_group(self.rhyhtmic_mode) 206 | 207 | def set_slice_notes_mode(self): 208 | self.set_mode_for_xor_group(self.slice_notes_mode) 209 | 210 | def set_pyramid_track_triggering_mode(self): 211 | self.set_mode_for_xor_group(self.pyramid_track_triggering_mode) 212 | 213 | def unset_pyramid_track_triggering_mode(self): 214 | self.unset_mode_for_xor_group(self.pyramid_track_triggering_mode) 215 | 216 | def set_preset_selection_mode(self): 217 | self.set_mode_for_xor_group(self.preset_selection_mode) 218 | 219 | def unset_preset_selection_mode(self): 220 | self.unset_mode_for_xor_group(self.preset_selection_mode) 221 | 222 | def save_current_settings_to_file(self): 223 | # NOTE: when saving device names, eliminate the last bit with XX:Y numbers as this might vary across runs 224 | # if different devices are connected 225 | settings = { 226 | 'midi_in_default_channel': self.midi_in_channel, 227 | 'midi_out_default_channel': self.midi_out_channel, 228 | 'default_midi_in_device_name': self.midi_in.name[:-4] if self.midi_in is not None else None, 229 | 'default_midi_out_device_name': self.midi_out.name[:-4] if self.midi_out is not None else None, 230 | 'default_notes_midi_in_device_name': self.notes_midi_in.name[:-4] if self.notes_midi_in is not None else None, 231 | 'use_push2_display': self.use_push2_display, 232 | 'target_frame_rate': self.target_frame_rate, 233 | } 234 | for mode in self.get_all_modes(): 235 | mode_settings = mode.get_settings_to_save() 236 | if mode_settings: 237 | settings.update(mode_settings) 238 | json.dump(settings, open('settings.json', 'w')) 239 | 240 | def init_midi_in(self, device_name=None): 241 | print('Configuring MIDI in to {}...'.format(device_name)) 242 | self.available_midi_in_device_names = [name for name in mido.get_input_names() if 'Ableton Push' not in name and 'RtMidi' not in name and 'Through' not in name] 243 | if device_name is not None: 244 | try: 245 | full_name = [name for name in self.available_midi_in_device_names if device_name in name][0] 246 | except IndexError: 247 | full_name = None 248 | if full_name is not None: 249 | if self.midi_in is not None: 250 | self.midi_in.callback = None # Disable current callback (if any) 251 | try: 252 | self.midi_in = mido.open_input(full_name) 253 | self.midi_in.callback = self.midi_in_handler 254 | print('Receiving MIDI in from "{0}"'.format(full_name)) 255 | except IOError: 256 | print('Could not connect to MIDI input port "{0}"\nAvailable device names:'.format(full_name)) 257 | for name in self.available_midi_in_device_names: 258 | print(' - {0}'.format(name)) 259 | else: 260 | print('No available device name found for {}'.format(device_name)) 261 | else: 262 | if self.midi_in is not None: 263 | self.midi_in.callback = None # Disable current callback (if any) 264 | self.midi_in.close() 265 | self.midi_in = None 266 | 267 | if self.midi_in is None: 268 | print('Not receiving from any MIDI input') 269 | 270 | def init_midi_out(self, device_name=None): 271 | print('Configuring MIDI out to {}...'.format(device_name)) 272 | self.available_midi_out_device_names = [name for name in mido.get_output_names() if 'Ableton Push' not in name and 'RtMidi' not in name and 'Through' not in name] 273 | self.available_midi_out_device_names += ['Virtual'] 274 | 275 | if device_name is not None: 276 | try: 277 | full_name = [name for name in self.available_midi_out_device_names if device_name in name][0] 278 | except IndexError: 279 | full_name = None 280 | if full_name is not None: 281 | try: 282 | if full_name == 'Virtual': 283 | self.midi_out = mido.open_output(full_name, virtual=True) 284 | else: 285 | self.midi_out = mido.open_output(full_name) 286 | print('Will send MIDI to "{0}"'.format(full_name)) 287 | except IOError: 288 | print('Could not connect to MIDI output port "{0}"\nAvailable device names:'.format(full_name)) 289 | for name in self.available_midi_out_device_names: 290 | print(' - {0}'.format(name)) 291 | else: 292 | print('No available device name found for {}'.format(device_name)) 293 | else: 294 | if self.midi_out is not None: 295 | self.midi_out.close() 296 | self.midi_out = None 297 | 298 | if self.midi_out is None: 299 | print('Won\'t send MIDI to any device') 300 | 301 | def init_notes_midi_in(self, device_name=None): 302 | print('Configuring notes MIDI in to {}...'.format(device_name)) 303 | self.available_midi_in_device_names = [name for name in mido.get_input_names() if 'Ableton Push' not in name and 'RtMidi' not in name and 'Through' not in name] 304 | 305 | if device_name is not None: 306 | try: 307 | full_name = [name for name in self.available_midi_in_device_names if device_name in name][0] 308 | except IndexError: 309 | full_name = None 310 | if full_name is not None: 311 | if self.notes_midi_in is not None: 312 | self.notes_midi_in.callback = None # Disable current callback (if any) 313 | try: 314 | self.notes_midi_in = mido.open_input(full_name) 315 | self.notes_midi_in.callback = self.notes_midi_in_handler 316 | print('Receiving notes MIDI in from "{0}"'.format(full_name)) 317 | except IOError: 318 | print('Could not connect to notes MIDI input port "{0}"\nAvailable device names:'.format(full_name)) 319 | for name in self.available_midi_in_device_names: 320 | print(' - {0}'.format(name)) 321 | else: 322 | print('No available device name found for {}'.format(device_name)) 323 | else: 324 | if self.notes_midi_in is not None: 325 | self.notes_midi_in.callback = None # Disable current callback (if any) 326 | self.notes_midi_in.close() 327 | self.notes_midi_in = None 328 | 329 | if self.notes_midi_in is None: 330 | print('Could not configures notes MIDI input') 331 | 332 | def set_midi_in_channel(self, channel, wrap=False): 333 | self.midi_in_channel = channel 334 | if self.midi_in_channel < -1: # Use "-1" for "all channels" 335 | self.midi_in_channel = -1 if not wrap else 15 336 | elif self.midi_in_channel > 15: 337 | self.midi_in_channel = 15 if not wrap else -1 338 | 339 | def set_midi_out_channel(self, channel, wrap=False): 340 | # We use channel -1 for the "track setting" in which midi channel is taken from currently selected track 341 | self.midi_out_channel = channel 342 | if self.midi_out_channel < -1: 343 | self.midi_out_channel = -1 if not wrap else 15 344 | elif self.midi_out_channel > 15: 345 | self.midi_out_channel = 15 if not wrap else -1 346 | 347 | def set_midi_in_device_by_index(self, device_idx): 348 | if device_idx >= 0 and device_idx < len(self.available_midi_in_device_names): 349 | self.init_midi_in(self.available_midi_in_device_names[device_idx]) 350 | else: 351 | self.init_midi_in(None) 352 | 353 | def set_midi_out_device_by_index(self, device_idx): 354 | if device_idx >= 0 and device_idx < len(self.available_midi_out_device_names): 355 | self.init_midi_out(self.available_midi_out_device_names[device_idx]) 356 | else: 357 | self.init_midi_out(None) 358 | 359 | def set_notes_midi_in_device_by_index(self, device_idx): 360 | if device_idx >= 0 and device_idx < len(self.available_midi_in_device_names): 361 | self.init_notes_midi_in(self.available_midi_in_device_names[device_idx]) 362 | else: 363 | self.init_notes_midi_in(None) 364 | 365 | def send_midi(self, msg, use_original_msg_channel=False): 366 | # Unless we specifically say we want to use the original msg mnidi channel, set it to global midi out channel or to the channel of the current track 367 | if not use_original_msg_channel and hasattr(msg, 'channel'): 368 | midi_out_channel = self.midi_out_channel 369 | if self.midi_out_channel == -1: 370 | # Send the message to the midi channel of the currently selected track (or to track 1 if selected track has no midi channel information) 371 | track_midi_channel = self.track_selection_mode.get_current_track_info()['midi_channel'] 372 | if track_midi_channel == -1: 373 | midi_out_channel = 0 374 | else: 375 | midi_out_channel = track_midi_channel - 1 # msg.channel is 0-indexed 376 | msg = msg.copy(channel=midi_out_channel) 377 | 378 | if self.midi_out is not None: 379 | self.midi_out.send(msg) 380 | 381 | 382 | def send_midi_to_pyramid(self, msg): 383 | # When sending to Pyramid, don't replace the MIDI channel because msg is already prepared with pyramidi chanel 384 | self.send_midi(msg, use_original_msg_channel=True) 385 | 386 | def midi_in_handler(self, msg): 387 | if hasattr(msg, 'channel'): # This will rule out sysex and other "strange" messages that don't have channel info 388 | if self.midi_in_channel == -1 or msg.channel == self.midi_in_channel: # If midi input channel is set to -1 (all) or a specific channel 389 | 390 | skip_message = False 391 | if msg.type == 'aftertouch': 392 | now = time.time() 393 | if (abs(self.last_cp_value_recevied - msg.value) > 10) and (now - self.last_cp_value_recevied_time < 0.5): 394 | skip_message = True 395 | else: 396 | self.last_cp_value_recevied = msg.value 397 | self.last_cp_value_recevied_time = time.time() 398 | 399 | if not skip_message: 400 | # Forward message to the main MIDI out 401 | self.send_midi(msg) 402 | 403 | # Forward the midi message to the active modes 404 | for mode in self.active_modes: 405 | mode.on_midi_in(msg, source=self.midi_in.name) 406 | 407 | def notes_midi_in_handler(self, msg): 408 | # Check if message is note on or off and check if the MIDI channel is the one assigned to the currently selected track 409 | # Then, send message to the melodic/rhythmic active modes so the notes are shown in pads/keys 410 | if msg.type == 'note_on' or msg.type == 'note_off': 411 | track_midi_channel = self.track_selection_mode.get_current_track_info()['midi_channel'] 412 | if msg.channel == track_midi_channel - 1: # msg.channel is 0-indexed 413 | for mode in self.active_modes: 414 | if mode == self.melodic_mode or mode == self.rhyhtmic_mode: 415 | mode.on_midi_in(msg, source=self.notes_midi_in.name) 416 | if mode.lumi_midi_out is not None: 417 | mode.lumi_midi_out.send(msg) 418 | else: 419 | # If midi not properly initialized try to re-initialize but don't do it too ofter 420 | if time.time() - mode.last_time_tried_initialize_lumi > 5: 421 | mode.init_lumi_midi_out() 422 | 423 | def add_display_notification(self, text): 424 | self.notification_text = text 425 | self.notification_time = time.time() 426 | 427 | def init_push(self): 428 | print('Configuring Push...') 429 | self.push = push2_python.Push2() 430 | if platform.system() == "Linux": 431 | # When this app runs in Linux is because it is running on the Raspberrypi 432 | # I've overved problems trying to reconnect many times withotu success on the Raspberrypi, resulting in 433 | # "ALSA lib seq_hw.c:466:(snd_seq_hw_open) open /dev/snd/seq failed: Cannot allocate memory" issues. 434 | # A work around is make the reconnection time bigger, but a better solution should probably be found. 435 | self.push.set_push2_reconnect_call_interval(2) 436 | 437 | def update_push2_pads(self): 438 | for mode in self.active_modes: 439 | mode.update_pads() 440 | 441 | def update_push2_buttons(self): 442 | for mode in self.active_modes: 443 | mode.update_buttons() 444 | 445 | def update_push2_display(self): 446 | if self.use_push2_display: 447 | # Prepare cairo canvas 448 | w, h = push2_python.constants.DISPLAY_LINE_PIXELS, push2_python.constants.DISPLAY_N_LINES 449 | surface = cairo.ImageSurface(cairo.FORMAT_RGB16_565, w, h) 450 | ctx = cairo.Context(surface) 451 | 452 | # Call all active modes to write to context 453 | for mode in self.active_modes: 454 | mode.update_display(ctx, w, h) 455 | 456 | # Show any notifications that should be shown 457 | if self.notification_text is not None: 458 | time_since_notification_started = time.time() - self.notification_time 459 | if time_since_notification_started < definitions.NOTIFICATION_TIME: 460 | show_notification(ctx, self.notification_text, opacity=1 - time_since_notification_started/definitions.NOTIFICATION_TIME) 461 | else: 462 | self.notification_text = None 463 | 464 | # Convert cairo data to numpy array and send to push 465 | buf = surface.get_data() 466 | frame = numpy.ndarray(shape=(h, w), dtype=numpy.uint16, buffer=buf).transpose() 467 | self.push.display.display_frame(frame, input_format=push2_python.constants.FRAME_FORMAT_RGB565) 468 | 469 | def check_for_delayed_actions(self): 470 | # If MIDI not configured, make sure we try sending messages so it gets configured 471 | if not self.push.midi_is_configured(): 472 | self.push.configure_midi() 473 | 474 | # Call dalyed actions in active modes 475 | for mode in self.active_modes: 476 | mode.check_for_delayed_actions() 477 | 478 | if self.pads_need_update: 479 | self.update_push2_pads() 480 | self.pads_need_update = False 481 | 482 | if self.buttons_need_update: 483 | self.update_push2_buttons() 484 | self.buttons_need_update = False 485 | 486 | def run_loop(self): 487 | print('Pysha is runnnig...') 488 | try: 489 | while True: 490 | before_draw_time = time.time() 491 | 492 | # Draw ui 493 | self.update_push2_display() 494 | 495 | # Frame rate measurement 496 | now = time.time() 497 | self.current_frame_rate_measurement += 1 498 | if time.time() - self.current_frame_rate_measurement_second > 1.0: 499 | self.actual_frame_rate = self.current_frame_rate_measurement 500 | self.current_frame_rate_measurement = 0 501 | self.current_frame_rate_measurement_second = now 502 | print('{0} fps'.format(self.actual_frame_rate)) 503 | 504 | # Check if any delayed actions need to be applied 505 | self.check_for_delayed_actions() 506 | 507 | after_draw_time = time.time() 508 | 509 | # Calculate sleep time to aproximate the target frame rate 510 | sleep_time = (1.0 / self.target_frame_rate) - (after_draw_time - before_draw_time) 511 | if sleep_time > 0: 512 | time.sleep(sleep_time) 513 | 514 | except KeyboardInterrupt: 515 | print('Exiting Pysha...') 516 | self.push.f_stop.set() 517 | 518 | def on_midi_push_connection_established(self): 519 | # Do initial configuration of Push 520 | print('Doing initial Push config...') 521 | 522 | # Force configure MIDI out (in case it wasn't...) 523 | app.push.configure_midi_out() 524 | 525 | # Configure custom color palette 526 | app.push.color_palette = {} 527 | for count, color_name in enumerate(definitions.COLORS_NAMES): 528 | app.push.set_color_palette_entry(count, [color_name, color_name], rgb=definitions.get_color_rgb_float(color_name), allow_overwrite=True) 529 | app.push.reapply_color_palette() 530 | 531 | # Initialize all buttons to black, initialize all pads to off 532 | app.push.buttons.set_all_buttons_color(color=definitions.BLACK) 533 | app.push.pads.set_all_pads_to_color(color=definitions.BLACK) 534 | 535 | # Iterate over modes and (re-)activate them 536 | for mode in self.active_modes: 537 | mode.activate() 538 | 539 | # Update buttons and pads (just in case something was missing!) 540 | app.update_push2_buttons() 541 | app.update_push2_pads() 542 | 543 | 544 | # Bind push action handlers with class methods 545 | @push2_python.on_encoder_rotated() 546 | def on_encoder_rotated(_, encoder_name, increment): 547 | try: 548 | for mode in app.active_modes[::-1]: 549 | action_performed = mode.on_encoder_rotated(encoder_name, increment) 550 | if action_performed: 551 | break # If mode took action, stop event propagation 552 | except NameError as e: 553 | print('Error: {}'.format(str(e))) 554 | traceback.print_exc() 555 | 556 | 557 | @push2_python.on_pad_pressed() 558 | def on_pad_pressed(_, pad_n, pad_ij, velocity): 559 | try: 560 | for mode in app.active_modes[::-1]: 561 | action_performed = mode.on_pad_pressed(pad_n, pad_ij, velocity) 562 | if action_performed: 563 | break # If mode took action, stop event propagation 564 | except NameError as e: 565 | print('Error: {}'.format(str(e))) 566 | traceback.print_exc() 567 | 568 | 569 | @push2_python.on_pad_released() 570 | def on_pad_released(_, pad_n, pad_ij, velocity): 571 | try: 572 | for mode in app.active_modes[::-1]: 573 | action_performed = mode.on_pad_released(pad_n, pad_ij, velocity) 574 | if action_performed: 575 | break # If mode took action, stop event propagation 576 | except NameError as e: 577 | print('Error: {}'.format(str(e))) 578 | traceback.print_exc() 579 | 580 | 581 | @push2_python.on_pad_aftertouch() 582 | def on_pad_aftertouch(_, pad_n, pad_ij, velocity): 583 | try: 584 | for mode in app.active_modes[::-1]: 585 | action_performed = mode.on_pad_aftertouch(pad_n, pad_ij, velocity) 586 | if action_performed: 587 | break # If mode took action, stop event propagation 588 | except NameError as e: 589 | print('Error: {}'.format(str(e))) 590 | traceback.print_exc() 591 | 592 | 593 | @push2_python.on_button_pressed() 594 | def on_button_pressed(_, name): 595 | try: 596 | for mode in app.active_modes[::-1]: 597 | action_performed = mode.on_button_pressed(name) 598 | if action_performed: 599 | break # If mode took action, stop event propagation 600 | except NameError as e: 601 | print('Error: {}'.format(str(e))) 602 | traceback.print_exc() 603 | 604 | 605 | @push2_python.on_button_released() 606 | def on_button_released(_, name): 607 | try: 608 | for mode in app.active_modes[::-1]: 609 | action_performed = mode.on_button_released(name) 610 | if action_performed: 611 | break # If mode took action, stop event propagation 612 | except NameError as e: 613 | print('Error: {}'.format(str(e))) 614 | traceback.print_exc() 615 | 616 | 617 | @push2_python.on_touchstrip() 618 | def on_touchstrip(_, value): 619 | try: 620 | for mode in app.active_modes[::-1]: 621 | action_performed = mode.on_touchstrip(value) 622 | if action_performed: 623 | break # If mode took action, stop event propagation 624 | except NameError as e: 625 | print('Error: {}'.format(str(e))) 626 | traceback.print_exc() 627 | 628 | 629 | @push2_python.on_sustain_pedal() 630 | def on_sustain_pedal(_, sustain_on): 631 | try: 632 | for mode in app.active_modes[::-1]: 633 | action_performed = mode.on_sustain_pedal(sustain_on) 634 | if action_performed: 635 | break # If mode took action, stop event propagation 636 | except NameError as e: 637 | print('Error: {}'.format(str(e))) 638 | traceback.print_exc() 639 | 640 | 641 | midi_connected_received_before_app = False 642 | 643 | 644 | @push2_python.on_midi_connected() 645 | def on_midi_connected(_): 646 | try: 647 | app.on_midi_push_connection_established() 648 | except NameError as e: 649 | global midi_connected_received_before_app 650 | midi_connected_received_before_app = True 651 | print('Error: {}'.format(str(e))) 652 | traceback.print_exc() 653 | 654 | 655 | # Run app main loop 656 | if __name__ == "__main__": 657 | app = PyshaApp() 658 | if midi_connected_received_before_app: 659 | # App received the "on_midi_connected" call before it was initialized. Do it now! 660 | print('Missed MIDI initialization call, doing it now...') 661 | app.on_midi_push_connection_established() 662 | app.run_loop() 663 | -------------------------------------------------------------------------------- /ddrm_tone_selector_mode.py: -------------------------------------------------------------------------------- 1 | import definitions 2 | import mido 3 | import push2_python 4 | import time 5 | import math 6 | import json 7 | import os 8 | 9 | from definitions import PyshaMode 10 | from display_utils import show_text 11 | 12 | 13 | NAME_STRING_1 = 'String\n1' 14 | NAME_STRING_2 = 'String\n2' 15 | NAME_STRING_3 = 'String\n3' 16 | NAME_STRING_4 = 'String\n4' 17 | NAME_BRASS_1 = 'Brass\n1' 18 | NAME_BRASS_2 = 'Brass\n2' 19 | NAME_BRASS_3 = 'Brass\n3' 20 | NAME_FLUTE = 'Flute' 21 | NAME_ELECTRIC_PIANO = 'Electric\nPiano' 22 | NAME_BASS = 'Bass' 23 | NAME_CLAVI_1 = 'Clavi-\nChord\n1' 24 | NAME_CLAVI_2 = 'Clavi-\nChord\n2' 25 | NAME_HARPSI_1 = 'Harpsi-\nChord\n1' 26 | NAME_HARPSI_2 = 'Harpsi-\nChord\n2' 27 | NAME_ORGAN_1 = 'Organ\n1' 28 | NAME_ORGAN_2 = 'Organ\n2' 29 | NAME_GUITAR_1 = 'Guitar\n1' 30 | NAME_GUITAR_2 = 'Guitar\n2' 31 | NAME_FUNKY_1 = 'Funky\n1' 32 | NAME_FUNKY_2 = 'Funky\n2' 33 | NAME_FUNKY_3 = 'Funky\n3' 34 | NAME_FUNKY_4 = 'Funky\n4' 35 | 36 | 37 | tone_selector_values = { # (MIDI CC for upper row, MIDI CC for lower row, MIDI value) 38 | NAME_BASS: [(40, 67, 0), 39 | (41, 68, 0), 40 | (42, 69, 55), 41 | (43, 71, 2), 42 | (44, 70, 1), 43 | (45, 72, 0), 44 | (46, 73, 0), 45 | (47, 119, 126), 46 | (48, 75, 34), 47 | (49, 76, 86), 48 | (50, 77, 26), 49 | (51, 78, 74), 50 | (52, 79, 13), 51 | (53, 80, 29), 52 | (54, 81, 71), 53 | (55, 82, 127), 54 | (56, 83, 0), 55 | (57, 84, 28), 56 | (58, 85, 92), 57 | (59, 86, 0), 58 | (60, 87, 41), 59 | (61, 88, 32), 60 | (62, 89, 86), 61 | (63, 90, 67), 62 | (65, 91, 0), 63 | (66, 92, 31)], 64 | NAME_BRASS_1: [(40, 67, 0), 65 | (41, 68, 0), 66 | (42, 69, 0), 67 | (43, 71, 127), 68 | (44, 70, 1), 69 | (45, 72, 0), 70 | (46, 73, 0), 71 | (47, 119, 0), 72 | (48, 75, 48), 73 | (49, 76, 4), 74 | (50, 77, 70), 75 | (51, 78, 57), 76 | (52, 79, 84), 77 | (53, 80, 63), 78 | (54, 81, 48), 79 | (55, 82, 127), 80 | (56, 83, 0), 81 | (57, 84, 59), 82 | (58, 85, 127), 83 | (59, 86, 125), 84 | (60, 87, 32), 85 | (61, 88, 0), 86 | (62, 89, 47), 87 | (63, 90, 126), 88 | (65, 91, 92), 89 | (66, 92, 107)], 90 | NAME_BRASS_2: [(40, 67, 0), 91 | (41, 68, 0), 92 | (42, 69, 0), 93 | (43, 71, 127), 94 | (44, 70, 1), 95 | (45, 72, 0), 96 | (46, 73, 0), 97 | (47, 119, 0), 98 | (48, 75, 46), 99 | (49, 76, 21), 100 | (50, 77, 109), 101 | (51, 78, 57), 102 | (52, 79, 83), 103 | (53, 80, 97), 104 | (54, 81, 51), 105 | (55, 82, 127), 106 | (56, 83, 0), 107 | (57, 84, 72), 108 | (58, 85, 102), 109 | (59, 86, 111), 110 | (60, 87, 13), 111 | (61, 88, 30), 112 | (62, 89, 51), 113 | (63, 90, 0), 114 | (65, 91, 93), 115 | (66, 92, 126)], 116 | NAME_BRASS_3: [(40, 67, 0), 117 | (41, 68, 0), 118 | (42, 69, 0), 119 | (43, 71, 127), 120 | (44, 70, 1), 121 | (45, 72, 0), 122 | (46, 73, 22), 123 | (47, 119, 0), 124 | (48, 75, 39), 125 | (49, 76, 59), 126 | (50, 77, 77), 127 | (51, 78, 86), 128 | (52, 79, 54), 129 | (53, 80, 43), 130 | (54, 81, 62), 131 | (55, 82, 127), 132 | (56, 83, 0), 133 | (57, 84, 59), 134 | (58, 85, 63), 135 | (59, 86, 57), 136 | (60, 87, 36), 137 | (61, 88, 27), 138 | (62, 89, 52), 139 | (63, 90, 99), 140 | (65, 91, 108), 141 | (66, 92, 122)], 142 | NAME_CLAVI_1: [(40, 67, 0), 143 | (41, 68, 0), 144 | (42, 69, 111), 145 | (43, 71, 1), 146 | (44, 70, 126), 147 | (45, 72, 0), 148 | (46, 73, 61), 149 | (47, 119, 64), 150 | (48, 75, 44), 151 | (49, 76, 56), 152 | (50, 77, 42), 153 | (51, 78, 45), 154 | (52, 79, 23), 155 | (53, 80, 86), 156 | (54, 81, 30), 157 | (55, 82, 127), 158 | (56, 83, 0), 159 | (57, 84, 0), 160 | (58, 85, 78), 161 | (59, 86, 47), 162 | (60, 87, 0), 163 | (61, 88, 0), 164 | (62, 89, 80), 165 | (63, 90, 127), 166 | (65, 91, 0), 167 | (66, 92, 0)], 168 | NAME_CLAVI_2: [(40, 67, 0), 169 | (41, 68, 0), 170 | (42, 69, 126), 171 | (43, 71, 0), 172 | (44, 70, 1), 173 | (45, 72, 0), 174 | (46, 73, 19), 175 | (47, 119, 0), 176 | (48, 75, 23), 177 | (49, 76, 42), 178 | (50, 77, 0), 179 | (51, 78, 127), 180 | (52, 79, 14), 181 | (53, 80, 62), 182 | (54, 81, 58), 183 | (55, 82, 127), 184 | (56, 83, 0), 185 | (57, 84, 0), 186 | (58, 85, 63), 187 | (59, 86, 49), 188 | (60, 87, 31), 189 | (61, 88, 0), 190 | (62, 89, 67), 191 | (63, 90, 127), 192 | (65, 91, 0), 193 | (66, 92, 0)], 194 | NAME_ELECTRIC_PIANO: [(40, 67, 0), 195 | (41, 68, 0), 196 | (42, 69, 53), 197 | (43, 71, 0), 198 | (44, 70, 126), 199 | (45, 72, 0), 200 | (46, 73, 0), 201 | (47, 119, 0), 202 | (48, 75, 35), 203 | (49, 76, 19), 204 | (50, 77, 75), 205 | (51, 78, 76), 206 | (52, 79, 0), 207 | (53, 80, 63), 208 | (54, 81, 56), 209 | (55, 82, 126), 210 | (56, 83, 0), 211 | (57, 84, 0), 212 | (58, 85, 65), 213 | (59, 86, 65), 214 | (60, 87, 64), 215 | (61, 88, 0), 216 | (62, 89, 53), 217 | (63, 90, 126), 218 | (65, 91, 0), 219 | (66, 92, 121)], 220 | NAME_FLUTE: [(40, 67, 0), 221 | (41, 68, 0), 222 | (42, 69, 0), 223 | (43, 71, 127), 224 | (44, 70, 0), 225 | (45, 72, 0), 226 | (46, 73, 0), 227 | (47, 119, 0), 228 | (48, 75, 46), 229 | (49, 76, 19), 230 | (50, 77, 0), 231 | (51, 78, 43), 232 | (52, 79, 85), 233 | (53, 80, 89), 234 | (54, 81, 20), 235 | (55, 82, 127), 236 | (56, 83, 0), 237 | (57, 84, 56), 238 | (58, 85, 127), 239 | (59, 86, 127), 240 | (60, 87, 17), 241 | (61, 88, 18), 242 | (62, 89, 35), 243 | (63, 90, 99), 244 | (65, 91, 41), 245 | (66, 92, 126)], 246 | NAME_FUNKY_1: [(40, 67, 62), 247 | (41, 68, 20), 248 | (42, 69, 89), 249 | (43, 71, 0), 250 | (44, 70, 0), 251 | (45, 72, 0), 252 | (46, 73, 18), 253 | (47, 119, 126), 254 | (48, 75, 19), 255 | (49, 76, 126), 256 | (50, 77, 126), 257 | (51, 78, 86), 258 | (52, 79, 0), 259 | (53, 80, 16), 260 | (54, 81, 56), 261 | (55, 82, 126), 262 | (56, 83, 0), 263 | (57, 84, 20), 264 | (58, 85, 127), 265 | (59, 86, 126), 266 | (60, 87, 0), 267 | (61, 88, 35), 268 | (62, 89, 126), 269 | (63, 90, 126), 270 | (65, 91, 126), 271 | (66, 92, 126)], 272 | NAME_FUNKY_2: [(40, 67, 0), 273 | (41, 68, 0), 274 | (42, 69, 0), 275 | (43, 71, 126), 276 | (44, 70, 1), 277 | (45, 72, 0), 278 | (46, 73, 39), 279 | (47, 119, 65), 280 | (48, 75, 21), 281 | (49, 76, 108), 282 | (50, 77, 126), 283 | (51, 78, 126), 284 | (52, 79, 57), 285 | (53, 80, 52), 286 | (54, 81, 0), 287 | (55, 82, 126), 288 | (56, 83, 0), 289 | (57, 84, 0), 290 | (58, 85, 60), 291 | (59, 86, 54), 292 | (60, 87, 0), 293 | (61, 88, 40), 294 | (62, 89, 50), 295 | (63, 90, 126), 296 | (65, 91, 0), 297 | (66, 92, 113)], 298 | NAME_FUNKY_3: [(40, 67, 83), 299 | (41, 68, 22), 300 | (42, 69, 49), 301 | (43, 71, 0), 302 | (44, 70, 126), 303 | (45, 72, 0), 304 | (46, 73, 18), 305 | (47, 119, 126), 306 | (48, 75, 5), 307 | (49, 76, 126), 308 | (50, 77, 126), 309 | (51, 78, 102), 310 | (52, 79, 124), 311 | (53, 80, 98), 312 | (54, 81, 50), 313 | (55, 82, 126), 314 | (56, 83, 81), 315 | (57, 84, 20), 316 | (58, 85, 127), 317 | (59, 86, 126), 318 | (60, 87, 0), 319 | (61, 88, 35), 320 | (62, 89, 126), 321 | (63, 90, 126), 322 | (65, 91, 85), 323 | (66, 92, 126)], 324 | NAME_GUITAR_1: [(40, 67, 0), 325 | (41, 68, 0), 326 | (42, 69, 80), 327 | (43, 71, 1), 328 | (44, 70, 127), 329 | (45, 72, 0), 330 | (46, 73, 52), 331 | (47, 119, 63), 332 | (48, 75, 66), 333 | (49, 76, 73), 334 | (50, 77, 56), 335 | (51, 78, 42), 336 | (52, 79, 17), 337 | (53, 80, 42), 338 | (54, 81, 46), 339 | (55, 82, 126), 340 | (56, 83, 0), 341 | (57, 84, 0), 342 | (58, 85, 70), 343 | (59, 86, 28), 344 | (60, 87, 43), 345 | (61, 88, 14), 346 | (62, 89, 50), 347 | (63, 90, 126), 348 | (65, 91, 14), 349 | (66, 92, 126)], 350 | NAME_GUITAR_2: [(40, 67, 0), 351 | (41, 68, 0), 352 | (42, 69, 30), 353 | (43, 71, 126), 354 | (44, 70, 1), 355 | (45, 72, 0), 356 | (46, 73, 39), 357 | (47, 119, 126), 358 | (48, 75, 32), 359 | (49, 76, 108), 360 | (50, 77, 89), 361 | (51, 78, 126), 362 | (52, 79, 11), 363 | (53, 80, 92), 364 | (54, 81, 37), 365 | (55, 82, 126), 366 | (56, 83, 0), 367 | (57, 84, 0), 368 | (58, 85, 95), 369 | (59, 86, 24), 370 | (60, 87, 14), 371 | (61, 88, 17), 372 | (62, 89, 23), 373 | (63, 90, 126), 374 | (65, 91, 0), 375 | (66, 92, 126)], 376 | NAME_HARPSI_1: [(40, 67, 0), 377 | (41, 68, 0), 378 | (42, 69, 99), 379 | (43, 71, 1), 380 | (44, 70, 126), 381 | (45, 72, 0), 382 | (46, 73, 81), 383 | (47, 119, 48), 384 | (48, 75, 77), 385 | (49, 76, 30), 386 | (50, 77, 0), 387 | (51, 78, 0), 388 | (52, 79, 17), 389 | (53, 80, 0), 390 | (54, 81, 0), 391 | (55, 82, 127), 392 | (56, 83, 0), 393 | (57, 84, 0), 394 | (58, 85, 73), 395 | (59, 86, 15), 396 | (60, 87, 20), 397 | (61, 88, 0), 398 | (62, 89, 0), 399 | (63, 90, 126), 400 | (65, 91, 0), 401 | (66, 92, 0)], 402 | NAME_HARPSI_2: [(40, 67, 0), 403 | (41, 68, 0), 404 | (42, 69, 100), 405 | (43, 71, 2), 406 | (44, 70, 126), 407 | (45, 72, 0), 408 | (46, 73, 64), 409 | (47, 119, 0), 410 | (48, 75, 79), 411 | (49, 76, 0), 412 | (50, 77, 0), 413 | (51, 78, 0), 414 | (52, 79, 13), 415 | (53, 80, 0), 416 | (54, 81, 0), 417 | (55, 82, 127), 418 | (56, 83, 0), 419 | (57, 84, 0), 420 | (58, 85, 75), 421 | (59, 86, 3), 422 | (60, 87, 0), 423 | (61, 88, 31), 424 | (62, 89, 0), 425 | (63, 90, 53), 426 | (65, 91, 0), 427 | (66, 92, 0)], 428 | NAME_ORGAN_1: [(40, 67, 0), 429 | (41, 68, 0), 430 | (42, 69, 0), 431 | (43, 71, 127), 432 | (44, 70, 1), 433 | (45, 72, 0), 434 | (46, 73, 54), 435 | (47, 119, 126), 436 | (48, 75, 60), 437 | (49, 76, 60), 438 | (50, 77, 0), 439 | (51, 78, 0), 440 | (52, 79, 14), 441 | (53, 80, 0), 442 | (54, 81, 0), 443 | (55, 82, 127), 444 | (56, 83, 0), 445 | (57, 84, 0), 446 | (58, 85, 0), 447 | (59, 86, 127), 448 | (60, 87, 0), 449 | (61, 88, 126), 450 | (62, 89, 0), 451 | (63, 90, 0), 452 | (65, 91, 0), 453 | (66, 92, 126)], 454 | NAME_ORGAN_2: [(40, 67, 0), 455 | (41, 68, 0), 456 | (42, 69, 0), 457 | (43, 71, 1), 458 | (44, 70, 127), 459 | (45, 72, 0), 460 | (46, 73, 0), 461 | (47, 119, 0), 462 | (48, 75, 64), 463 | (49, 76, 127), 464 | (50, 77, 0), 465 | (51, 78, 0), 466 | (52, 79, 0), 467 | (53, 80, 0), 468 | (54, 81, 0), 469 | (55, 82, 127), 470 | (56, 83, 0), 471 | (57, 84, 0), 472 | (58, 85, 23), 473 | (59, 86, 38), 474 | (60, 87, 0), 475 | (61, 88, 127), 476 | (62, 89, 0), 477 | (63, 90, 0), 478 | (65, 91, 0), 479 | (66, 92, 126)], 480 | NAME_STRING_1: [(40, 67, 0), 481 | (41, 68, 0), 482 | (42, 69, 0), 483 | (43, 71, 126), 484 | (44, 70, 0), 485 | (45, 72, 0), 486 | (46, 73, 70), 487 | (47, 119, 73), 488 | (48, 75, 84), 489 | (49, 76, 80), 490 | (50, 77, 80), 491 | (51, 78, 79), 492 | (52, 79, 78), 493 | (53, 80, 66), 494 | (54, 81, 19), 495 | (55, 82, 127), 496 | (56, 83, 0), 497 | (57, 84, 85), 498 | (58, 85, 127), 499 | (59, 86, 88), 500 | (60, 87, 0), 501 | (61, 88, 24), 502 | (62, 89, 59), 503 | (63, 90, 126), 504 | (65, 91, 0), 505 | (66, 92, 0)], 506 | NAME_STRING_2: [(40, 67, 0), 507 | (41, 68, 0), 508 | (42, 69, 0), 509 | (43, 71, 127), 510 | (44, 70, 2), 511 | (45, 72, 0), 512 | (46, 73, 57), 513 | (47, 119, 0), 514 | (48, 75, 126), 515 | (49, 76, 0), 516 | (50, 77, 0), 517 | (51, 78, 0), 518 | (52, 79, 0), 519 | (53, 80, 0), 520 | (54, 81, 0), 521 | (55, 82, 127), 522 | (56, 83, 0), 523 | (57, 84, 35), 524 | (58, 85, 127), 525 | (59, 86, 127), 526 | (60, 87, 34), 527 | (61, 88, 0), 528 | (62, 89, 0), 529 | (63, 90, 37), 530 | (65, 91, 0), 531 | (66, 92, 126)], 532 | NAME_STRING_3: [(40, 67, 0), 533 | (41, 68, 0), 534 | (42, 69, 0), 535 | (43, 71, 127), 536 | (44, 70, 0), 537 | (45, 72, 0), 538 | (46, 73, 0), 539 | (47, 119, 0), 540 | (48, 75, 99), 541 | (49, 76, 0), 542 | (50, 77, 0), 543 | (51, 78, 0), 544 | (52, 79, 14), 545 | (53, 80, 0), 546 | (54, 81, 0), 547 | (55, 82, 127), 548 | (56, 83, 0), 549 | (57, 84, 98), 550 | (58, 85, 79), 551 | (59, 86, 126), 552 | (60, 87, 52), 553 | (61, 88, 57), 554 | (62, 89, 0), 555 | (63, 90, 106), 556 | (65, 91, 0), 557 | (66, 92, 126)], 558 | NAME_STRING_4: [(40, 67, 0), 559 | (41, 68, 0), 560 | (42, 69, 0), 561 | (43, 71, 127), 562 | (44, 70, 2), 563 | (45, 72, 0), 564 | (46, 73, 57), 565 | (47, 119, 10), 566 | (48, 75, 73), 567 | (49, 76, 0), 568 | (50, 77, 49), 569 | (51, 78, 49), 570 | (52, 79, 53), 571 | (53, 80, 83), 572 | (54, 81, 50), 573 | (55, 82, 127), 574 | (56, 83, 0), 575 | (57, 84, 69), 576 | (58, 85, 86), 577 | (59, 86, 127), 578 | (60, 87, 46), 579 | (61, 88, 36), 580 | (62, 89, 44), 581 | (63, 90, 20), 582 | (65, 91, 65), 583 | (66, 92, 126)]} 584 | 585 | 586 | class DDRMToneSelectorMode(PyshaMode): 587 | 588 | upper_row_button_names = [ 589 | push2_python.constants.BUTTON_UPPER_ROW_1, 590 | push2_python.constants.BUTTON_UPPER_ROW_2, 591 | push2_python.constants.BUTTON_UPPER_ROW_3, 592 | push2_python.constants.BUTTON_UPPER_ROW_4, 593 | push2_python.constants.BUTTON_UPPER_ROW_5, 594 | push2_python.constants.BUTTON_UPPER_ROW_6, 595 | push2_python.constants.BUTTON_UPPER_ROW_7, 596 | push2_python.constants.BUTTON_UPPER_ROW_8 597 | ] 598 | 599 | lower_row_button_names = [ 600 | push2_python.constants.BUTTON_LOWER_ROW_1, 601 | push2_python.constants.BUTTON_LOWER_ROW_2, 602 | push2_python.constants.BUTTON_LOWER_ROW_3, 603 | push2_python.constants.BUTTON_LOWER_ROW_4, 604 | push2_python.constants.BUTTON_LOWER_ROW_5, 605 | push2_python.constants.BUTTON_LOWER_ROW_6, 606 | push2_python.constants.BUTTON_LOWER_ROW_7, 607 | push2_python.constants.BUTTON_LOWER_ROW_8 608 | ] 609 | 610 | upper_row_names = [ 611 | NAME_STRING_1, 612 | NAME_STRING_3, 613 | NAME_BRASS_1, 614 | NAME_FLUTE, 615 | NAME_ELECTRIC_PIANO, 616 | NAME_CLAVI_1, 617 | NAME_HARPSI_1, 618 | NAME_ORGAN_1, 619 | NAME_GUITAR_1, 620 | NAME_FUNKY_1, 621 | NAME_FUNKY_3, 622 | ] 623 | 624 | lower_row_names = [ 625 | NAME_STRING_2, 626 | NAME_STRING_4, 627 | NAME_BRASS_2, 628 | NAME_BRASS_3, 629 | NAME_BASS, 630 | NAME_CLAVI_2, 631 | NAME_HARPSI_2, 632 | NAME_ORGAN_2, 633 | NAME_GUITAR_2, 634 | NAME_FUNKY_2, 635 | #NAME_FUNKY_4 636 | ] 637 | 638 | colors = { 639 | NAME_STRING_1: definitions.YELLOW, 640 | NAME_STRING_3: definitions.YELLOW, 641 | NAME_BRASS_1: definitions.RED, 642 | NAME_FLUTE: definitions.WHITE, 643 | NAME_ELECTRIC_PIANO: definitions.YELLOW, 644 | NAME_CLAVI_1: definitions.YELLOW, 645 | NAME_HARPSI_1: definitions.YELLOW, 646 | NAME_ORGAN_1: definitions.WHITE, 647 | NAME_GUITAR_1: definitions.YELLOW, 648 | NAME_FUNKY_1: definitions.GREEN, 649 | NAME_FUNKY_3: definitions.GREEN, 650 | NAME_STRING_2: definitions.YELLOW, 651 | NAME_STRING_4: definitions.YELLOW, 652 | NAME_BRASS_2: definitions.RED, 653 | NAME_BRASS_3: definitions.RED, 654 | NAME_BASS: definitions.WHITE, 655 | NAME_CLAVI_2: definitions.YELLOW, 656 | NAME_HARPSI_2: definitions.YELLOW, 657 | NAME_ORGAN_2: definitions.WHITE, 658 | NAME_GUITAR_2: definitions.YELLOW, 659 | NAME_FUNKY_2: definitions.GREEN, 660 | NAME_FUNKY_4: definitions.GREEN 661 | } 662 | 663 | font_colors = { 664 | definitions.YELLOW: definitions.BLACK, 665 | definitions.RED: definitions.WHITE, 666 | definitions.WHITE: definitions.BLACK, 667 | definitions.GREEN: definitions.WHITE 668 | } 669 | 670 | page_n = 0 671 | upper_row_selected = '' 672 | lower_row_selected = '' 673 | inter_message_message_min_time_ms = 4 # ms wait time after each message to DDRM 674 | send_messages_double = False # This is a workaround for a DDRM bug that will ignore single CC messages. We'll send 2 messages in a row for the same control with slightly different values 675 | 676 | def should_be_enabled(self): 677 | return self.app.track_selection_mode.get_current_track_instrument_short_name() == "DDRM" 678 | 679 | def get_should_show_next_prev(self): 680 | show_prev = self.page_n == 1 681 | show_next = self.page_n == 0 682 | return show_prev, show_next 683 | 684 | def send_lower_row(self): 685 | if self.lower_row_selected in tone_selector_values: 686 | for _, midi_cc, midi_val in tone_selector_values[self.lower_row_selected]: 687 | if self.send_messages_double: 688 | values_to_send = [(midi_val + 1) % 128, midi_val] 689 | else: 690 | values_to_send = [midi_val] 691 | for val in values_to_send: 692 | msg = mido.Message('control_change', control=midi_cc, value=val) # Should we subtract 1 from midi_cc because mido being 0-indexed? 693 | self.app.send_midi(msg) 694 | if self.inter_message_message_min_time_ms: 695 | time.sleep(self.inter_message_message_min_time_ms*1.0/1000) 696 | 697 | def send_upper_row(self): 698 | if self.upper_row_selected in tone_selector_values: 699 | for midi_cc, _, midi_val in tone_selector_values[self.upper_row_selected]: 700 | if self.send_messages_double: 701 | values_to_send = [(midi_val + 1) % 128, midi_val] 702 | else: 703 | values_to_send = [midi_val] 704 | for val in values_to_send: 705 | msg = mido.Message('control_change', control=midi_cc, value=val) # Should we subtract 1 from midi_cc because mido being 0-indexed? 706 | self.app.send_midi(msg) 707 | if self.inter_message_message_min_time_ms: 708 | time.sleep(self.inter_message_message_min_time_ms*1.0/1000) 709 | 710 | def activate(self): 711 | self.update_buttons() 712 | 713 | def deactivate(self): 714 | for button_name in self.upper_row_button_names + self.lower_row_button_names + [push2_python.constants.BUTTON_PAGE_LEFT, push2_python.constants.BUTTON_PAGE_RIGHT]: 715 | self.push.buttons.set_button_color(button_name, definitions.BLACK) 716 | 717 | def update_buttons(self): 718 | 719 | for count, name in enumerate(self.upper_row_button_names): 720 | try: 721 | tone_name = self.upper_row_names[count + self.page_n * 8] 722 | self.push.buttons.set_button_color(name, self.colors[tone_name]) 723 | except IndexError: 724 | self.push.buttons.set_button_color(name, definitions.OFF_BTN_COLOR) 725 | 726 | for count, name in enumerate(self.lower_row_button_names): 727 | try: 728 | tone_name = self.lower_row_names[count + self.page_n * 8] 729 | self.push.buttons.set_button_color(name, self.colors[tone_name]) 730 | except IndexError: 731 | self.push.buttons.set_button_color(name, definitions.OFF_BTN_COLOR) 732 | 733 | show_prev, show_next = self.get_should_show_next_prev() 734 | if show_prev: 735 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_PAGE_LEFT, definitions.WHITE) 736 | else: 737 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_PAGE_LEFT, definitions.BLACK) 738 | if show_next: 739 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_PAGE_RIGHT, definitions.WHITE) 740 | else: 741 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_PAGE_RIGHT, definitions.BLACK) 742 | 743 | def update_display(self, ctx, w, h): 744 | 745 | if not self.app.is_mode_active(self.app.settings_mode): 746 | # If settings mode is active, don't draw the upper parts of the screen because settings page will 747 | # "cover them" 748 | 749 | start = self.page_n * 8 750 | 751 | # Draw upper row 752 | for i, name in enumerate(self.upper_row_names[start:][:8]): 753 | font_color = self.font_colors[self.colors[name]] 754 | if name == self.upper_row_selected: 755 | background_color = self.colors[name] 756 | else: 757 | background_color = self.colors[name] + '_darker1' 758 | height = 80 759 | top_offset = 0 760 | show_text(ctx, i, top_offset, name.upper(), height=height, font_color=font_color, background_color=background_color, 761 | font_size_percentage=0.2, center_vertically=True, center_horizontally=True, rectangle_padding=1) 762 | 763 | # Draw lower row 764 | for i, name in enumerate(self.lower_row_names[start:][:8]): 765 | if name != NAME_FUNKY_4: 766 | font_color = self.font_colors[self.colors[name]] 767 | if name == self.lower_row_selected: 768 | background_color = self.colors[name] 769 | else: 770 | background_color = self.colors[name] + '_darker1' 771 | height = 80 772 | top_offset = 80 773 | show_text(ctx, i, top_offset, name.upper(), height=height, 774 | font_color=font_color, background_color=background_color, font_size_percentage=0.2, center_vertically=True, center_horizontally=True, rectangle_padding=1) 775 | 776 | def on_button_pressed(self, button_name): 777 | if button_name in self.upper_row_button_names: 778 | start = self.page_n * 8 779 | button_idx = self.upper_row_button_names.index(button_name) 780 | try: 781 | self.upper_row_selected = self.upper_row_names[button_idx + start] 782 | self.send_upper_row() 783 | except IndexError: 784 | # Do nothing because the button has no assigned tone 785 | pass 786 | return True 787 | 788 | elif button_name in self.lower_row_button_names: 789 | start = self.page_n * 8 790 | button_idx = self.lower_row_button_names.index(button_name) 791 | try: 792 | self.lower_row_selected = self.lower_row_names[button_idx + start] 793 | self.send_lower_row() 794 | except IndexError: 795 | # Do nothing because the button has no assigned tone 796 | pass 797 | return True 798 | 799 | elif button_name in [push2_python.constants.BUTTON_PAGE_LEFT, push2_python.constants.BUTTON_PAGE_RIGHT]: 800 | show_prev, show_next = self.get_should_show_next_prev() 801 | if button_name == push2_python.constants.BUTTON_PAGE_LEFT and show_prev: 802 | self.page_n = 0 803 | elif button_name == push2_python.constants.BUTTON_PAGE_RIGHT and show_next: 804 | self.page_n = 1 805 | self.app.buttons_need_update = True 806 | return True 807 | -------------------------------------------------------------------------------- /definitions.py: -------------------------------------------------------------------------------- 1 | import push2_python 2 | import colorsys 3 | 4 | VERSION = '0.25' 5 | 6 | DELAYED_ACTIONS_APPLY_TIME = 1.0 # Encoder changes won't be applied until this time has passed since last moved 7 | 8 | LAYOUT_MELODIC = 'lmelodic' 9 | LAYOUT_RHYTHMIC = 'lrhythmic' 10 | LAYOUT_SLICES = 'lslices' 11 | 12 | NOTIFICATION_TIME = 3 13 | 14 | BLACK_RGB = [0, 0, 0] 15 | GRAY_DARK_RGB = [30, 30, 30] 16 | GRAY_LIGHT_RGB = [180, 180, 180] 17 | WHITE_RGB = [255, 255, 255] 18 | YELLOW_RGB = [255, 241, 0] 19 | ORANGE_RGB = [255, 140, 0] 20 | RED_RGB = [232, 17, 35] 21 | PINK_RGB = [236, 0, 140] 22 | PURPLE_RGB = [104, 33, 122] 23 | BLUE_RGB = [0, 24, 143] 24 | CYAN_RGB = [0, 188, 242] 25 | TURQUOISE_RGB = [0, 178, 148] 26 | GREEN_RGB = [0, 158, 73] 27 | LIME_RGB = [186, 216, 10] 28 | 29 | BLACK = 'black' 30 | GRAY_DARK = 'gray_dark' 31 | GRAY_LIGHT = 'gray_light' 32 | WHITE = 'white' 33 | YELLOW = 'yellow' 34 | ORANGE = 'orange' 35 | RED = 'red' 36 | PINK = 'pink' 37 | PURPLE = 'purple' 38 | BLUE = 'blue' 39 | CYAN = 'cyan' 40 | TURQUOISE = 'turquoise' 41 | GREEN = 'green' 42 | LIME = 'lime' 43 | 44 | COLORS_NAMES = [ORANGE, YELLOW, TURQUOISE, LIME, RED, PINK, PURPLE, BLUE, CYAN, GREEN, BLACK, GRAY_DARK, GRAY_LIGHT, WHITE] 45 | 46 | def get_color_rgb(color_name): 47 | return globals().get('{0}_RGB'.format(color_name.upper()), [0, 0, 0]) 48 | 49 | def get_color_rgb_float(color_name): 50 | return [x/255 for x in get_color_rgb(color_name)] 51 | 52 | 53 | # Create darker1 and darker2 versions of each color in COLOR_NAMES, add new colors back to COLOR_NAMES 54 | to_add_in_color_names = [] 55 | for name in COLORS_NAMES: 56 | 57 | # Create darker 1 58 | color_mod = 0.35 # < 1 means make colour darker, > 1 means make colour brighter 59 | c = colorsys.rgb_to_hls(*get_color_rgb_float(name)) 60 | darker_color = colorsys.hls_to_rgb(c[0], max(0, min(1, color_mod * c[1])), c[2]) 61 | new_color_name = f'{name}_darker1' 62 | globals()[new_color_name.upper()] = new_color_name 63 | if new_color_name not in COLORS_NAMES: 64 | to_add_in_color_names.append(new_color_name) 65 | new_color_rgb_name = f'{name}_darker1_rgb' 66 | globals()[new_color_rgb_name.upper()] = list([c * 255 for c in darker_color]) 67 | 68 | # Create darker 2 69 | color_mod = 0.05 # < 1 means make colour darker, > 1 means make colour brighter 70 | c = colorsys.rgb_to_hls(*get_color_rgb_float(name)) 71 | darker_color = colorsys.hls_to_rgb(c[0], max(0, min(1, color_mod * c[1])), c[2]) 72 | new_color_name = f'{name}_darker2' 73 | globals()[new_color_name.upper()] = new_color_name 74 | if new_color_name not in COLORS_NAMES: 75 | to_add_in_color_names.append(new_color_name) 76 | new_color_rgb_name = f'{name}_darker2_rgb' 77 | globals()[new_color_rgb_name.upper()] = list([c * 255 for c in darker_color]) 78 | 79 | COLORS_NAMES += to_add_in_color_names # Update list of color names with darkified versiond of existing colors 80 | 81 | FONT_COLOR_DELAYED_ACTIONS = ORANGE 82 | FONT_COLOR_DISABLED = GRAY_LIGHT 83 | OFF_BTN_COLOR = GRAY_DARK 84 | NOTE_ON_COLOR = GREEN 85 | 86 | DEFAULT_ANIMATION = push2_python.constants.ANIMATION_PULSING_QUARTER 87 | 88 | INSTRUMENT_DEFINITION_FOLDER = 'instrument_definitions' 89 | TRACK_LISTING_PATH = 'track_listing.json' 90 | 91 | class PyshaMode(object): 92 | """ 93 | """ 94 | 95 | name = '' 96 | xor_group = None 97 | 98 | def __init__(self, app, settings=None): 99 | self.app = app 100 | self.initialize(settings=settings) 101 | 102 | @property 103 | def push(self): 104 | return self.app.push 105 | 106 | # Method run only once when the mode object is created, may receive settings dictionary from main app 107 | def initialize(self, settings=None): 108 | pass 109 | 110 | # Method to return a dictionary of properties to store in a settings file, and that will be passed to 111 | # initialize method when object created 112 | def get_settings_to_save(self): 113 | return {} 114 | 115 | # Methods that are run before the mode is activated and when it is deactivated 116 | def activate(self): 117 | pass 118 | 119 | def deactivate(self): 120 | pass 121 | 122 | # Method called at every iteration in the main loop to see if any actions need to be performed at the end of the iteration 123 | # This is used to avoid some actions unncessesarily being repeated many times 124 | def check_for_delayed_actions(self): 125 | pass 126 | 127 | # Method called when MIDI messages arrive from Pysha MIDI input 128 | def on_midi_in(self, msg, source=None): 129 | pass 130 | 131 | # Push2 update methods 132 | def update_pads(self): 133 | pass 134 | 135 | def update_buttons(self): 136 | pass 137 | 138 | def update_display(self, ctx, w, h): 139 | pass 140 | 141 | # Push2 action callbacks (these methods should return True if some action was carried out, otherwise return None) 142 | def on_encoder_rotated(self, encoder_name, increment): 143 | pass 144 | 145 | def on_button_pressed(self, button_name): 146 | pass 147 | 148 | def on_button_released(self, button_name): 149 | pass 150 | 151 | 152 | def on_pad_pressed(self, pad_n, pad_ij, velocity): 153 | pass 154 | 155 | def on_pad_released(self, pad_n, pad_ij, velocity): 156 | pass 157 | 158 | def on_pad_aftertouch(self, pad_n, pad_ij, velocity): 159 | pass 160 | 161 | def on_touchstrip(self, value): 162 | pass 163 | 164 | def on_sustain_pedal(self, sustain_on): 165 | pass 166 | -------------------------------------------------------------------------------- /display_utils.py: -------------------------------------------------------------------------------- 1 | import cairo 2 | import definitions 3 | import push2_python 4 | 5 | 6 | def show_title(ctx, x, h, text, color=[1, 1, 1]): 7 | text = str(text) 8 | ctx.set_source_rgb(*color) 9 | ctx.select_font_face("Arial", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL) 10 | font_size = h//12 11 | ctx.set_font_size(font_size) 12 | ctx.move_to(x + 3, 20) 13 | ctx.show_text(text) 14 | 15 | 16 | def show_value(ctx, x, h, text, color=[1, 1, 1]): 17 | text = str(text) 18 | ctx.set_source_rgb(*color) 19 | ctx.select_font_face("Arial", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL) 20 | font_size = h//8 21 | ctx.set_font_size(font_size) 22 | ctx.move_to(x + 3, 45) 23 | ctx.show_text(text) 24 | 25 | 26 | def draw_text_at(ctx, x, y, text, font_size = 12, color=[1, 1, 1]): 27 | text = str(text) 28 | ctx.set_source_rgb(*color) 29 | ctx.select_font_face("Arial", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL) 30 | ctx.set_font_size(font_size) 31 | ctx.move_to(x, y) 32 | ctx.show_text(text) 33 | 34 | 35 | def show_text(ctx, x_part, pixels_from_top, text, height=20, font_color=definitions.WHITE, background_color=None, margin_left=4, margin_top=4, font_size_percentage=0.8, center_vertically=True, center_horizontally=False, rectangle_padding=0): 36 | assert 0 <= x_part < 8 37 | assert type(x_part) == int 38 | 39 | display_w = push2_python.constants.DISPLAY_LINE_PIXELS 40 | display_h = push2_python.constants.DISPLAY_N_LINES 41 | part_w = display_w // 8 42 | x1 = part_w * x_part 43 | y1 = pixels_from_top 44 | 45 | ctx.save() 46 | 47 | if background_color is not None: 48 | ctx.set_source_rgb(*definitions.get_color_rgb_float(background_color)) 49 | ctx.rectangle(x1 + rectangle_padding, y1 + rectangle_padding, part_w - rectangle_padding * 2, height - rectangle_padding * 2) 50 | ctx.fill() 51 | ctx.set_source_rgb(*definitions.get_color_rgb_float(font_color)) 52 | ctx.select_font_face("Arial", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL) 53 | font_size = round(int(height * font_size_percentage)) 54 | text_lines = text.split('\n') 55 | n_lines = len(text_lines) 56 | if center_vertically: 57 | margin_top = (height - font_size * n_lines) // 2 58 | ctx.set_font_size(font_size) 59 | for i, line in enumerate(text_lines): 60 | if center_horizontally: 61 | (_, _, l_width, _, _, _) = ctx.text_extents(line) 62 | ctx.move_to(x1 + part_w/2 - l_width/2, y1 + font_size * (i + 1) + margin_top - 2) 63 | else: 64 | ctx.move_to(x1 + margin_left, y1 + font_size * (i + 1) + margin_top - 2) 65 | ctx.show_text(line) 66 | 67 | ctx.restore() 68 | 69 | def show_notification(ctx, text, opacity=1.0): 70 | ctx.save() 71 | 72 | # Background 73 | display_w = push2_python.constants.DISPLAY_LINE_PIXELS 74 | display_h = push2_python.constants.DISPLAY_N_LINES 75 | initial_bg_opacity = 0.8 76 | ctx.set_source_rgba(0.0, 0.0, 0.0, initial_bg_opacity * opacity) 77 | ctx.rectangle(0, 0, display_w, display_h) 78 | ctx.fill() 79 | 80 | # Text 81 | initial_text_opacity = 1.0 82 | ctx.set_source_rgba(1.0, 1.0, 1.0, initial_text_opacity * opacity) 83 | font_size = display_h // 4 84 | ctx.set_font_size(font_size) 85 | margin_left = 8 86 | ctx.move_to(margin_left, 2.2 * font_size) 87 | ctx.show_text(text) 88 | 89 | ctx.restore() 90 | -------------------------------------------------------------------------------- /docs/diagram.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ffont/pysha/716d5e21abc425312c56a7a29f10b48d4e64f8a2/docs/diagram.jpeg -------------------------------------------------------------------------------- /docs/diagram.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ffont/pysha/716d5e21abc425312c56a7a29f10b48d4e64f8a2/docs/diagram.key -------------------------------------------------------------------------------- /docs/pysha_instrument_selection_midi_cc.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ffont/pysha/716d5e21abc425312c56a7a29f10b48d4e64f8a2/docs/pysha_instrument_selection_midi_cc.jpeg -------------------------------------------------------------------------------- /docs/pysha_melodic_mode.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ffont/pysha/716d5e21abc425312c56a7a29f10b48d4e64f8a2/docs/pysha_melodic_mode.jpeg -------------------------------------------------------------------------------- /docs/pysha_rhythmic_mode.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ffont/pysha/716d5e21abc425312c56a7a29f10b48d4e64f8a2/docs/pysha_rhythmic_mode.jpeg -------------------------------------------------------------------------------- /docs/pysha_velocity_curves.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ffont/pysha/716d5e21abc425312c56a7a29f10b48d4e64f8a2/docs/pysha_velocity_curves.jpeg -------------------------------------------------------------------------------- /instrument_definitions/DOMINION.json: -------------------------------------------------------------------------------- 1 | { 2 | "instrument_name": "Dominion", 3 | "instrument_short_name": "DOMINION", 4 | "illuminate_local_notes": false, 5 | "midi_channel": 3, 6 | "n_banks": 1 7 | } -------------------------------------------------------------------------------- /instrument_definitions/MINITAUR.json: -------------------------------------------------------------------------------- 1 | { 2 | "instrument_name": "Minitaur", 3 | "instrument_short_name": "MINITAUR", 4 | "illuminate_local_notes": false, 5 | "midi_channel": 2, 6 | "n_banks": 1, 7 | "midi_cc": [ 8 | { 9 | "section": "VCO", 10 | "controls": [ 11 | [ 12 | "VCO1 LEV", 13 | 15 14 | ], 15 | [ 16 | "VCO2 LEV", 17 | 16 18 | ], 19 | [ 20 | "VCO1 WAVE", 21 | 70 22 | ], 23 | [ 24 | "VCO2 WAVE", 25 | 71 26 | ], 27 | [ 28 | "GLIDE RATE", 29 | 5 30 | ], 31 | [ 32 | "LEG GLIDE", 33 | 83 34 | ], 35 | [ 36 | "GLIDE TYPE", 37 | 92 38 | ], 39 | [ 40 | "GLIDE SWITCH", 41 | 65 42 | ], 43 | [ 44 | "VCO2FREQ", 45 | 17 46 | ], 47 | [ 48 | "VCO2BEAT", 49 | 18 50 | ] 51 | ] 52 | }, 53 | { 54 | "section": "VCF", 55 | "controls": [ 56 | [ 57 | "CUTOFF", 58 | 19 59 | ], 60 | [ 61 | "RESSO", 62 | 21 63 | ], 64 | [ 65 | "KB TRACK", 66 | 20 67 | ], 68 | [ 69 | "EG AMT", 70 | 22 71 | ], 72 | [ 73 | "ATTACK", 74 | 23 75 | ], 76 | [ 77 | "DEC/REL", 78 | 24 79 | ], 80 | [ 81 | "SUSTAIN", 82 | 25 83 | ] 84 | ] 85 | }, 86 | { 87 | "section": "VCA", 88 | "controls": [ 89 | [ 90 | "OUT LEV", 91 | 7 92 | ], 93 | [ 94 | "ATTACK", 95 | 28 96 | ], 97 | [ 98 | "DEC/REL", 99 | 29 100 | ], 101 | [ 102 | "SUSTAIN", 103 | 30 104 | ] 105 | ] 106 | }, 107 | { 108 | "section": "LFO", 109 | "controls": [ 110 | [ 111 | "RATE", 112 | 3 113 | ], 114 | [ 115 | "VCO AMT", 116 | 13 117 | ], 118 | [ 119 | "VCF AMT", 120 | 12 121 | ], 122 | [ 123 | "CLOCK DIV", 124 | 86 125 | ] 126 | ] 127 | }, 128 | { 129 | "section": "OTHER", 130 | "controls": [ 131 | [ 132 | "FILT VEL", 133 | 89 134 | ], 135 | [ 136 | "VOL VEL", 137 | 90 138 | ], 139 | [ 140 | "EXT IN", 141 | 27 142 | ], 143 | [ 144 | "REL SWITCH", 145 | 72 146 | ], 147 | [ 148 | "TRIG MODE", 149 | 73 150 | ], 151 | [ 152 | "NOTE SYNC", 153 | 81 154 | ], 155 | [ 156 | "KEY PRIOR", 157 | 91 158 | ], 159 | [ 160 | "BEND UP", 161 | 107 162 | ], 163 | [ 164 | "BEND DOWN", 165 | 108 166 | ] 167 | ] 168 | } 169 | ] 170 | } -------------------------------------------------------------------------------- /instrument_definitions/OCTATRACK.json: -------------------------------------------------------------------------------- 1 | { 2 | "instrument_name": "Octatrack", 3 | "instrument_short_name": "OCTATRACK", 4 | "illuminate_local_notes": true, 5 | "n_banks": 1, 6 | "default_layout": "lslices" 7 | } -------------------------------------------------------------------------------- /instrument_definitions/SOURCE.json: -------------------------------------------------------------------------------- 1 | { 2 | "instrument_name": "Source", 3 | "instrument_short_name": "SOURCE", 4 | "illuminate_local_notes": false, 5 | "midi_channel": 5, 6 | "default_layout": "lrhythmic", 7 | "n_banks": 1 8 | } -------------------------------------------------------------------------------- /main_controls_mode.py: -------------------------------------------------------------------------------- 1 | import definitions 2 | import push2_python 3 | import time 4 | 5 | TOGGLE_DISPLAY_BUTTON = push2_python.constants.BUTTON_USER 6 | SETTINGS_BUTTON = push2_python.constants.BUTTON_SETUP 7 | MELODIC_RHYTHMIC_TOGGLE_BUTTON = push2_python.constants.BUTTON_NOTE 8 | PYRAMID_TRACK_TRIGGERING_BUTTON = push2_python.constants.BUTTON_ADD_TRACK 9 | PRESET_SELECTION_MODE_BUTTON = push2_python.constants.BUTTON_ADD_DEVICE 10 | DDRM_TONE_SELECTION_MODE_BUTTON = push2_python.constants.BUTTON_DEVICE 11 | 12 | 13 | class MainControlsMode(definitions.PyshaMode): 14 | 15 | pyramid_track_triggering_button_pressing_time = None 16 | preset_selection_button_pressing_time = None 17 | button_quick_press_time = 0.400 18 | 19 | def activate(self): 20 | self.update_buttons() 21 | 22 | def deactivate(self): 23 | self.push.buttons.set_button_color(MELODIC_RHYTHMIC_TOGGLE_BUTTON, definitions.BLACK) 24 | self.push.buttons.set_button_color(TOGGLE_DISPLAY_BUTTON, definitions.BLACK) 25 | self.push.buttons.set_button_color(SETTINGS_BUTTON, definitions.BLACK) 26 | self.push.buttons.set_button_color(PYRAMID_TRACK_TRIGGERING_BUTTON, definitions.BLACK) 27 | self.push.buttons.set_button_color(PRESET_SELECTION_MODE_BUTTON, definitions.BLACK) 28 | self.push.buttons.set_button_color(DDRM_TONE_SELECTION_MODE_BUTTON, definitions.BLACK) 29 | 30 | def update_buttons(self): 31 | # Note button, to toggle melodic/rhythmic mode 32 | self.push.buttons.set_button_color(MELODIC_RHYTHMIC_TOGGLE_BUTTON, definitions.WHITE) 33 | 34 | # Mute button, to toggle display on/off 35 | if self.app.use_push2_display: 36 | self.push.buttons.set_button_color(TOGGLE_DISPLAY_BUTTON, definitions.WHITE) 37 | else: 38 | self.push.buttons.set_button_color(TOGGLE_DISPLAY_BUTTON, definitions.OFF_BTN_COLOR) 39 | 40 | # Settings button, to toggle settings mode 41 | if self.app.is_mode_active(self.app.settings_mode): 42 | self.push.buttons.set_button_color(SETTINGS_BUTTON, definitions.BLACK) 43 | self.push.buttons.set_button_color(SETTINGS_BUTTON, definitions.WHITE, animation=definitions.DEFAULT_ANIMATION) 44 | else: 45 | self.push.buttons.set_button_color(SETTINGS_BUTTON, definitions.OFF_BTN_COLOR) 46 | 47 | # Pyramid track triggering mode 48 | if self.app.is_mode_active(self.app.pyramid_track_triggering_mode): 49 | self.push.buttons.set_button_color(PYRAMID_TRACK_TRIGGERING_BUTTON, definitions.BLACK) 50 | self.push.buttons.set_button_color(PYRAMID_TRACK_TRIGGERING_BUTTON, definitions.WHITE, animation=definitions.DEFAULT_ANIMATION) 51 | else: 52 | self.push.buttons.set_button_color(PYRAMID_TRACK_TRIGGERING_BUTTON, definitions.OFF_BTN_COLOR) 53 | 54 | # Preset selection mode 55 | if self.app.is_mode_active(self.app.preset_selection_mode): 56 | self.push.buttons.set_button_color(PRESET_SELECTION_MODE_BUTTON, definitions.BLACK) 57 | self.push.buttons.set_button_color(PRESET_SELECTION_MODE_BUTTON, definitions.WHITE, animation=definitions.DEFAULT_ANIMATION) 58 | else: 59 | self.push.buttons.set_button_color(PRESET_SELECTION_MODE_BUTTON, definitions.OFF_BTN_COLOR) 60 | 61 | # DDRM tone selector mode 62 | if self.app.ddrm_tone_selector_mode.should_be_enabled(): 63 | if self.app.is_mode_active(self.app.ddrm_tone_selector_mode): 64 | self.push.buttons.set_button_color(DDRM_TONE_SELECTION_MODE_BUTTON, definitions.BLACK) 65 | self.push.buttons.set_button_color(DDRM_TONE_SELECTION_MODE_BUTTON, definitions.WHITE, animation=definitions.DEFAULT_ANIMATION) 66 | else: 67 | self.push.buttons.set_button_color(DDRM_TONE_SELECTION_MODE_BUTTON, definitions.OFF_BTN_COLOR) 68 | else: 69 | self.push.buttons.set_button_color(DDRM_TONE_SELECTION_MODE_BUTTON, definitions.BLACK) 70 | 71 | def on_button_pressed(self, button_name): 72 | if button_name == MELODIC_RHYTHMIC_TOGGLE_BUTTON: 73 | self.app.toggle_melodic_rhythmic_slice_modes() 74 | self.app.pads_need_update = True 75 | self.app.buttons_need_update = True 76 | return True 77 | elif button_name == SETTINGS_BUTTON: 78 | self.app.toggle_and_rotate_settings_mode() 79 | self.app.buttons_need_update = True 80 | return True 81 | elif button_name == TOGGLE_DISPLAY_BUTTON: 82 | self.app.use_push2_display = not self.app.use_push2_display 83 | if not self.app.use_push2_display: 84 | self.push.display.send_to_display(self.push.display.prepare_frame(self.push.display.make_black_frame())) 85 | self.app.buttons_need_update = True 86 | return True 87 | elif button_name == PYRAMID_TRACK_TRIGGERING_BUTTON: 88 | if self.app.is_mode_active(self.app.pyramid_track_triggering_mode): 89 | # If already active, deactivate and set pressing time to None 90 | self.app.unset_pyramid_track_triggering_mode() 91 | self.pyramid_track_triggering_button_pressing_time = None 92 | else: 93 | # Activate track triggering mode and store time button pressed 94 | self.app.set_pyramid_track_triggering_mode() 95 | self.pyramid_track_triggering_button_pressing_time = time.time() 96 | self.app.buttons_need_update = True 97 | return True 98 | elif button_name == PRESET_SELECTION_MODE_BUTTON: 99 | if self.app.is_mode_active(self.app.preset_selection_mode): 100 | # If already active, deactivate and set pressing time to None 101 | self.app.unset_preset_selection_mode() 102 | self.preset_selection_button_pressing_time = None 103 | else: 104 | # Activate preset selection mode and store time button pressed 105 | self.app.set_preset_selection_mode() 106 | self.preset_selection_button_pressing_time = time.time() 107 | self.app.buttons_need_update = True 108 | return True 109 | elif button_name == DDRM_TONE_SELECTION_MODE_BUTTON: 110 | if self.app.ddrm_tone_selector_mode.should_be_enabled(): 111 | self.app.toggle_ddrm_tone_selector_mode() 112 | self.app.buttons_need_update = True 113 | return True 114 | 115 | def on_button_released(self, button_name): 116 | if button_name == PYRAMID_TRACK_TRIGGERING_BUTTON: 117 | # Decide if short press or long press 118 | pressing_time = self.pyramid_track_triggering_button_pressing_time 119 | is_long_press = False 120 | if pressing_time is None: 121 | # Consider quick press (this should not happen pressing time should have been set before) 122 | pass 123 | else: 124 | if time.time() - pressing_time > self.button_quick_press_time: 125 | # Consider this is a long press 126 | is_long_press = True 127 | self.pyramid_track_triggering_button_pressing_time = None 128 | 129 | if is_long_press: 130 | # If long press, deactivate track triggering mode, else do nothing 131 | self.app.unset_pyramid_track_triggering_mode() 132 | self.app.buttons_need_update = True 133 | 134 | return True 135 | 136 | elif button_name == PRESET_SELECTION_MODE_BUTTON: 137 | # Decide if short press or long press 138 | pressing_time = self.preset_selection_button_pressing_time 139 | is_long_press = False 140 | if pressing_time is None: 141 | # Consider quick press (this should not happen pressing time should have been set before) 142 | pass 143 | else: 144 | if time.time() - pressing_time > self.button_quick_press_time: 145 | # Consider this is a long press 146 | is_long_press = True 147 | self.preset_selection_button_pressing_time = None 148 | 149 | if is_long_press: 150 | # If long press, deactivate preset selection mode, else do nothing 151 | self.app.unset_preset_selection_mode() 152 | self.app.buttons_need_update = True 153 | 154 | return True 155 | -------------------------------------------------------------------------------- /melodic_mode.py: -------------------------------------------------------------------------------- 1 | import definitions 2 | import mido 3 | import push2_python.constants 4 | import time 5 | 6 | 7 | class MelodicMode(definitions.PyshaMode): 8 | 9 | xor_group = 'pads' 10 | 11 | notes_being_played = [] 12 | root_midi_note = 0 # default redefined in initialize 13 | scale_pattern = [True, False, True, False, True, True, False, True, False, True, False, True] 14 | fixed_velocity_mode = False 15 | use_poly_at = False # default redefined in initialize 16 | channel_at_range_start = 401 # default redefined in initialize 17 | channel_at_range_end = 800 # default redefined in initialize 18 | poly_at_max_range = 40 # default redefined in initialize 19 | poly_at_curve_bending = 50 # default redefined in initialize 20 | latest_channel_at_value = (0, 0) 21 | latest_poly_at_value = (0, 0) 22 | latest_velocity_value = (0, 0) 23 | last_time_at_params_edited = None 24 | modulation_wheel_mode = False 25 | 26 | lumi_midi_out = None 27 | last_time_tried_initialize_lumi = 0 28 | 29 | def init_lumi_midi_out(self): 30 | print('Configuring LUMI notes MIDI out...') 31 | self.last_time_tried_initialize_lumi = time.time() 32 | device_name = "LUMI Keys BLOCK" 33 | try: 34 | full_name = [name for name in mido.get_output_names() if device_name.lower() in name.lower()][0] 35 | except IndexError: 36 | full_name = None 37 | 38 | if full_name is not None: 39 | try: 40 | self.lumi_midi_out = mido.open_output(full_name) 41 | print('Sending notes MIDI in to "{0}"'.format(full_name)) 42 | except IOError: 43 | print('Could not connect to notes MIDI output port "{0}"\nAvailable device names:'.format(full_name)) 44 | else: 45 | print('No available device name found for {}'.format(device_name)) 46 | 47 | def set_lumi_pressure_mode(self): 48 | if self.lumi_midi_out is not None: 49 | if not self.use_poly_at: 50 | msg = mido.Message('sysex', data=[0x00, 0x21, 0x10, 0x77, 0x27, 0x10, 0x00, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64]) 51 | else: 52 | msg = mido.Message('sysex', data=[0x00, 0x21, 0x10, 0x77, 0x27, 0x10, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04]) 53 | self.lumi_midi_out.send(msg) 54 | 55 | def initialize(self, settings=None): 56 | if settings is not None: 57 | self.use_poly_at = settings.get('use_poly_at', True) 58 | self.set_root_midi_note(settings.get('root_midi_note', 64)) 59 | self.channel_at_range_start = settings.get('channel_at_range_start', 401) 60 | self.channel_at_range_end = settings.get('channel_at_range_end', 800) 61 | self.poly_at_max_range = settings.get('poly_at_max_range', 40) 62 | self.poly_at_curve_bending = settings.get('poly_at_curve_bending', 50) 63 | self.init_lumi_midi_out() 64 | 65 | def get_settings_to_save(self): 66 | return { 67 | 'use_poly_at': self.use_poly_at, 68 | 'root_midi_note': self.root_midi_note, 69 | 'channel_at_range_start': self.channel_at_range_start, 70 | 'channel_at_range_end': self.channel_at_range_end, 71 | 'poly_at_max_range': self.poly_at_max_range, 72 | 'poly_at_curve_bending': self.poly_at_curve_bending, 73 | } 74 | 75 | def set_channel_at_range_start(self, value): 76 | # Parameter in range [401, channel_at_range_end - 1] 77 | if value < 401: 78 | value = 401 79 | elif value >= self.channel_at_range_end: 80 | value = self.channel_at_range_end - 1 81 | self.channel_at_range_start = value 82 | self.last_time_at_params_edited = time.time() 83 | 84 | def set_channel_at_range_end(self, value): 85 | # Parameter in range [channel_at_range_start + 1, 2000] 86 | if value <= self.channel_at_range_start: 87 | value = self.channel_at_range_start + 1 88 | elif value > 2000: 89 | value = 2000 90 | self.channel_at_range_end = value 91 | self.last_time_at_params_edited = time.time() 92 | 93 | def set_poly_at_max_range(self, value): 94 | # Parameter in range [0, 127] 95 | if value < 0: 96 | value = 0 97 | elif value > 127: 98 | value = 127 99 | self.poly_at_max_range = value 100 | self.last_time_at_params_edited = time.time() 101 | 102 | def set_poly_at_curve_bending(self, value): 103 | # Parameter in range [0, 100] 104 | if value < 0: 105 | value = 0 106 | elif value > 100: 107 | value = 100 108 | self.poly_at_curve_bending = value 109 | self.last_time_at_params_edited = time.time() 110 | 111 | def get_poly_at_curve(self): 112 | pow_curve = [pow(e, 3*self.poly_at_curve_bending/100) for e in [i/self.poly_at_max_range for i in range(0, self.poly_at_max_range)]] 113 | return [int(127 * pow_curve[i]) if i < self.poly_at_max_range else 127 for i in range(0, 128)] 114 | 115 | def add_note_being_played(self, midi_note, source): 116 | self.notes_being_played.append({'note': midi_note, 'source': source}) 117 | 118 | def remove_note_being_played(self, midi_note, source): 119 | self.notes_being_played = [note for note in self.notes_being_played if note['note'] != midi_note or note['source'] != source] 120 | 121 | def remove_all_notes_being_played(self): 122 | self.notes_being_played = [] 123 | 124 | def pad_ij_to_midi_note(self, pad_ij): 125 | return self.root_midi_note + ((7 - pad_ij[0]) * 5 + pad_ij[1]) 126 | 127 | def is_midi_note_root_octave(self, midi_note): 128 | relative_midi_note = (midi_note - self.root_midi_note) % 12 129 | return relative_midi_note == 0 130 | 131 | def is_black_key_midi_note(self, midi_note): 132 | relative_midi_note = (midi_note - self.root_midi_note) % 12 133 | return not self.scale_pattern[relative_midi_note] 134 | 135 | def is_midi_note_being_played(self, midi_note): 136 | for note in self.notes_being_played: 137 | if note['note'] == midi_note: 138 | return True 139 | return False 140 | 141 | def note_number_to_name(self, note_number): 142 | semis = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'] 143 | note_number = int(round(note_number)) 144 | return semis[note_number % 12] + str(note_number//12 - 2) 145 | 146 | def set_root_midi_note(self, note_number): 147 | self.root_midi_note = note_number 148 | if self.root_midi_note < 0: 149 | self.root_midi_note = 0 150 | elif self.root_midi_note > 127: 151 | self.root_midi_note = 127 152 | 153 | def activate(self): 154 | 155 | # Configure polyAT and AT 156 | if self.use_poly_at: 157 | self.push.pads.set_polyphonic_aftertouch() 158 | else: 159 | self.push.pads.set_channel_aftertouch() 160 | self.push.pads.set_channel_aftertouch_range(range_start=self.channel_at_range_start, range_end=self.channel_at_range_end) 161 | self.push.pads.set_velocity_curve(velocities=self.get_poly_at_curve()) 162 | 163 | self.set_lumi_pressure_mode() 164 | 165 | # Configure touchstrip behaviour 166 | if self.modulation_wheel_mode: 167 | self.push.touchstrip.set_modulation_wheel_mode() 168 | else: 169 | self.push.touchstrip.set_pitch_bend_mode() 170 | 171 | # Update buttons and pads 172 | self.update_buttons() 173 | self.update_pads() 174 | 175 | def deactivate(self): 176 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_OCTAVE_DOWN, definitions.BLACK) 177 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_OCTAVE_UP, definitions.BLACK) 178 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_ACCENT, definitions.BLACK) 179 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_SHIFT, definitions.BLACK) 180 | 181 | def check_for_delayed_actions(self): 182 | if self.last_time_at_params_edited is not None and time.time() - self.last_time_at_params_edited > definitions.DELAYED_ACTIONS_APPLY_TIME: 183 | # Update channel and poly AT parameters 184 | self.push.pads.set_channel_aftertouch_range(range_start=self.channel_at_range_start, range_end=self.channel_at_range_end) 185 | self.push.pads.set_velocity_curve(velocities=self.get_poly_at_curve()) 186 | self.last_time_at_params_edited = None 187 | 188 | def on_midi_in(self, msg, source=None): 189 | # Update the list of notes being currently played so push2 pads can be updated accordingly 190 | if msg.type == "note_on": 191 | if msg.velocity == 0: 192 | self.remove_note_being_played(msg.note, source) 193 | else: 194 | self.add_note_being_played(msg.note, source) 195 | elif msg.type == "note_off": 196 | self.remove_note_being_played(msg.note, source) 197 | self.app.pads_need_update = True 198 | 199 | def update_octave_buttons(self): 200 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_OCTAVE_DOWN, definitions.WHITE) 201 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_OCTAVE_UP, definitions.WHITE) 202 | 203 | def update_accent_button(self): 204 | if self.fixed_velocity_mode: 205 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_ACCENT, definitions.BLACK) 206 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_ACCENT, definitions.WHITE, animation=definitions.DEFAULT_ANIMATION) 207 | else: 208 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_ACCENT, definitions.OFF_BTN_COLOR) 209 | 210 | def update_modulation_wheel_mode_button(self): 211 | if self.modulation_wheel_mode: 212 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_SHIFT, definitions.BLACK) 213 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_SHIFT, definitions.WHITE, animation=definitions.DEFAULT_ANIMATION) 214 | else: 215 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_SHIFT, definitions.OFF_BTN_COLOR) 216 | 217 | def update_buttons(self): 218 | self.update_octave_buttons() 219 | self.update_modulation_wheel_mode_button() 220 | self.update_accent_button() 221 | 222 | def update_pads(self): 223 | color_matrix = [] 224 | for i in range(0, 8): 225 | row_colors = [] 226 | for j in range(0, 8): 227 | corresponding_midi_note = self.pad_ij_to_midi_note([i, j]) 228 | cell_color = definitions.WHITE 229 | if self.is_black_key_midi_note(corresponding_midi_note): 230 | cell_color = definitions.BLACK 231 | if self.is_midi_note_root_octave(corresponding_midi_note): 232 | try: 233 | cell_color = self.app.track_selection_mode.get_current_track_color() 234 | except AttributeError: 235 | cell_color = definitions.YELLOW 236 | if self.is_midi_note_being_played(corresponding_midi_note): 237 | cell_color = definitions.NOTE_ON_COLOR 238 | 239 | row_colors.append(cell_color) 240 | color_matrix.append(row_colors) 241 | 242 | self.push.pads.set_pads_color(color_matrix) 243 | 244 | def on_pad_pressed(self, pad_n, pad_ij, velocity): 245 | midi_note = self.pad_ij_to_midi_note(pad_ij) 246 | if midi_note is not None: 247 | self.latest_velocity_value = (time.time(), velocity) 248 | if self.app.track_selection_mode.get_current_track_info().get('illuminate_local_notes', True) or self.app.notes_midi_in is None: 249 | # illuminate_local_notes is used to decide wether a pad/key should be lighted when pressing it. This will probably be the default behaviour, 250 | # but in synth definitions this can be disabled because we will be receiving back note events at the "notes_midi_in" device and in this 251 | # case we don't want to light the pad "twice" (or if the note pressed gets processed and another note is actually played we don't want to 252 | # light the currently presed pad). However, if "notes_midi_in" input is not configured, we do want to liht the pad as we won't have 253 | # notes info comming from any other source 254 | self.add_note_being_played(midi_note, 'push') 255 | msg = mido.Message('note_on', note=midi_note, velocity=velocity if not self.fixed_velocity_mode else 127) 256 | self.app.send_midi(msg) 257 | self.update_pads() # Directly calling update pads method because we want user to feel feedback as quick as possible 258 | return True 259 | 260 | def on_pad_released(self, pad_n, pad_ij, velocity): 261 | midi_note = self.pad_ij_to_midi_note(pad_ij) 262 | if midi_note is not None: 263 | if self.app.track_selection_mode.get_current_track_info().get('illuminate_local_notes', True) or self.app.notes_midi_in is None: 264 | # see comment in "on_pad_pressed" above 265 | self.remove_note_being_played(midi_note, 'push') 266 | msg = mido.Message('note_off', note=midi_note, velocity=velocity) 267 | self.app.send_midi(msg) 268 | self.update_pads() # Directly calling update pads method because we want user to feel feedback as quick as possible 269 | return True 270 | 271 | def on_pad_aftertouch(self, pad_n, pad_ij, velocity): 272 | if pad_n is not None: 273 | # polyAT mode 274 | self.latest_poly_at_value = (time.time(), velocity) 275 | midi_note = self.pad_ij_to_midi_note(pad_ij) 276 | if midi_note is not None: 277 | msg = mido.Message('polytouch', note=midi_note, value=velocity) 278 | else: 279 | # channel AT mode 280 | self.latest_channel_at_value = (time.time(), velocity) 281 | msg = mido.Message('aftertouch', value=velocity) 282 | self.app.send_midi(msg) 283 | return True 284 | 285 | def on_touchstrip(self, value): 286 | if self.modulation_wheel_mode: 287 | msg = mido.Message('control_change', control=1, value=value) 288 | else: 289 | msg = mido.Message('pitchwheel', pitch=value) 290 | self.app.send_midi(msg) 291 | return True 292 | 293 | def on_sustain_pedal(self, sustain_on): 294 | msg = mido.Message('control_change', control=64, value=127 if sustain_on else 0) 295 | self.app.send_midi(msg) 296 | return True 297 | 298 | def on_button_pressed(self, button_name): 299 | if button_name == push2_python.constants.BUTTON_OCTAVE_UP: 300 | self.set_root_midi_note(self.root_midi_note + 12) 301 | self.app.pads_need_update = True 302 | self.app.add_display_notification("Octave up: from {0} to {1}".format( 303 | self.note_number_to_name(self.pad_ij_to_midi_note((7, 0))), 304 | self.note_number_to_name(self.pad_ij_to_midi_note((0, 7))), 305 | )) 306 | return True 307 | 308 | elif button_name == push2_python.constants.BUTTON_OCTAVE_DOWN: 309 | self.set_root_midi_note(self.root_midi_note - 12) 310 | self.app.pads_need_update = True 311 | self.app.add_display_notification("Octave down: from {0} to {1}".format( 312 | self.note_number_to_name(self.pad_ij_to_midi_note((7, 0))), 313 | self.note_number_to_name(self.pad_ij_to_midi_note((0, 7))), 314 | )) 315 | return True 316 | 317 | elif button_name == push2_python.constants.BUTTON_ACCENT: 318 | self.fixed_velocity_mode = not self.fixed_velocity_mode 319 | self.app.buttons_need_update = True 320 | self.app.pads_need_update = True 321 | self.app.add_display_notification("Fixed velocity: {0}".format('On' if self.fixed_velocity_mode else 'Off')) 322 | return True 323 | 324 | elif button_name == push2_python.constants.BUTTON_SHIFT: 325 | self.modulation_wheel_mode = not self.modulation_wheel_mode 326 | if self.modulation_wheel_mode: 327 | self.push.touchstrip.set_modulation_wheel_mode() 328 | else: 329 | self.push.touchstrip.set_pitch_bend_mode() 330 | self.app.buttons_need_update = True 331 | self.app.add_display_notification("Touchstrip mode: {0}".format('Modulation wheel' if self.modulation_wheel_mode else 'Pitch bend')) 332 | return True 333 | -------------------------------------------------------------------------------- /midi_cc_mode.py: -------------------------------------------------------------------------------- 1 | import definitions 2 | import mido 3 | import push2_python 4 | import time 5 | import math 6 | import json 7 | import os 8 | 9 | from definitions import PyshaMode, OFF_BTN_COLOR 10 | from display_utils import show_text 11 | 12 | 13 | class MIDICCControl(object): 14 | 15 | color = definitions.GRAY_LIGHT 16 | color_rgb = None 17 | name = 'Unknown' 18 | section = 'unknown' 19 | cc_number = 10 # 0-127 20 | value = 64 21 | vmin = 0 22 | vmax = 127 23 | get_color_func = None 24 | send_midi_func = None 25 | value_labels_map = {} 26 | 27 | def __init__(self, cc_number, name, section_name, get_color_func, send_midi_func): 28 | self.cc_number = cc_number 29 | self.name = name 30 | self.section = section_name 31 | self.get_color_func = get_color_func 32 | self.send_midi_func = send_midi_func 33 | 34 | def draw(self, ctx, x_part): 35 | margin_top = 25 36 | 37 | # Param name 38 | name_height = 20 39 | show_text(ctx, x_part, margin_top, self.name, height=name_height, font_color=definitions.WHITE) 40 | 41 | # Param value 42 | val_height = 30 43 | color = self.get_color_func() 44 | show_text(ctx, x_part, margin_top + name_height, self.value_labels_map.get(str(self.value), str(self.value)), height=val_height, font_color=color) 45 | 46 | # Knob 47 | ctx.save() 48 | 49 | circle_break_degrees = 80 50 | height = 55 51 | radius = height/2 52 | 53 | display_w = push2_python.constants.DISPLAY_LINE_PIXELS 54 | x = (display_w // 8) * x_part 55 | y = margin_top + name_height + val_height + radius + 5 56 | 57 | start_rad = (90 + circle_break_degrees // 2) * (math.pi / 180) 58 | end_rad = (90 - circle_break_degrees // 2) * (math.pi / 180) 59 | xc = x + radius + 3 60 | yc = y 61 | 62 | def get_rad_for_value(value): 63 | total_degrees = 360 - circle_break_degrees 64 | return start_rad + total_degrees * ((value - self.vmin)/(self.vmax - self.vmin)) * (math.pi / 180) 65 | 66 | # This is needed to prevent showing line from previous position 67 | ctx.set_source_rgb(0, 0, 0) 68 | ctx.move_to(xc, yc) 69 | ctx.stroke() 70 | 71 | # Inner circle 72 | ctx.arc(xc, yc, radius, start_rad, end_rad) 73 | ctx.set_source_rgb(*definitions.get_color_rgb_float(definitions.GRAY_LIGHT)) 74 | ctx.set_line_width(1) 75 | ctx.stroke() 76 | 77 | # Outer circle 78 | ctx.arc(xc, yc, radius, start_rad, get_rad_for_value(self.value)) 79 | ctx.set_source_rgb(* definitions.get_color_rgb_float(color)) 80 | ctx.set_line_width(3) 81 | ctx.stroke() 82 | 83 | ctx.restore() 84 | 85 | def update_value(self, increment): 86 | if self.value + increment > self.vmax: 87 | self.value = self.vmax 88 | elif self.value + increment < self.vmin: 89 | self.value = self.vmin 90 | else: 91 | self.value += increment 92 | 93 | # Send cc message, subtract 1 to number because MIDO works from 0 - 127 94 | msg = mido.Message('control_change', control=self.cc_number, value=self.value) 95 | self.send_midi_func(msg) 96 | 97 | 98 | class MIDICCMode(PyshaMode): 99 | 100 | midi_cc_button_names = [ 101 | push2_python.constants.BUTTON_UPPER_ROW_1, 102 | push2_python.constants.BUTTON_UPPER_ROW_2, 103 | push2_python.constants.BUTTON_UPPER_ROW_3, 104 | push2_python.constants.BUTTON_UPPER_ROW_4, 105 | push2_python.constants.BUTTON_UPPER_ROW_5, 106 | push2_python.constants.BUTTON_UPPER_ROW_6, 107 | push2_python.constants.BUTTON_UPPER_ROW_7, 108 | push2_python.constants.BUTTON_UPPER_ROW_8 109 | ] 110 | instrument_midi_control_ccs = {} 111 | active_midi_control_ccs = [] 112 | current_selected_section_and_page = {} 113 | 114 | def initialize(self, settings=None): 115 | for instrument_short_name in self.get_all_distinct_instrument_short_names_helper(): 116 | try: 117 | midi_cc = json.load(open(os.path.join(definitions.INSTRUMENT_DEFINITION_FOLDER, '{}.json'.format(instrument_short_name)))).get('midi_cc', None) 118 | except FileNotFoundError: 119 | midi_cc = None 120 | 121 | if midi_cc is not None: 122 | # Create MIDI CC mappings for instruments with definitions 123 | self.instrument_midi_control_ccs[instrument_short_name] = [] 124 | for section in midi_cc: 125 | section_name = section['section'] 126 | for name, cc_number in section['controls']: 127 | control = MIDICCControl(cc_number, name, section_name, self.get_current_track_color_helper, self.app.send_midi) 128 | if section.get('control_value_label_maps', {}).get(name, False): 129 | control.value_labels_map = section['control_value_label_maps'][name] 130 | self.instrument_midi_control_ccs[instrument_short_name].append(control) 131 | print('Loaded {0} MIDI cc mappings for instrument {1}'.format(len(self.instrument_midi_control_ccs[instrument_short_name]), instrument_short_name)) 132 | else: 133 | # No definition file for instrument exists, or no midi CC were defined for that instrument 134 | self.instrument_midi_control_ccs[instrument_short_name] = [] 135 | for i in range(0, 128): 136 | section_s = (i // 16) * 16 137 | section_e = section_s + 15 138 | control = MIDICCControl(i, 'CC {0}'.format(i), '{0} to {1}'.format(section_s, section_e), self.get_current_track_color_helper, self.app.send_midi) 139 | self.instrument_midi_control_ccs[instrument_short_name].append(control) 140 | print('Loaded default MIDI cc mappings for instrument {0}'.format(instrument_short_name)) 141 | 142 | # Fill in current page and section variables 143 | for instrument_short_name in self.instrument_midi_control_ccs: 144 | self.current_selected_section_and_page[instrument_short_name] = (self.instrument_midi_control_ccs[instrument_short_name][0].section, 0) 145 | 146 | def get_all_distinct_instrument_short_names_helper(self): 147 | return self.app.track_selection_mode.get_all_distinct_instrument_short_names() 148 | 149 | def get_current_track_color_helper(self): 150 | return self.app.track_selection_mode.get_current_track_color() 151 | 152 | def get_current_track_instrument_short_name_helper(self): 153 | return self.app.track_selection_mode.get_current_track_instrument_short_name() 154 | 155 | def get_current_track_midi_cc_sections(self): 156 | section_names = [] 157 | for control in self.instrument_midi_control_ccs.get(self.get_current_track_instrument_short_name_helper(), []): 158 | section_name = control.section 159 | if section_name not in section_names: 160 | section_names.append(section_name) 161 | return section_names 162 | 163 | def get_currently_selected_midi_cc_section_and_page(self): 164 | return self.current_selected_section_and_page[self.get_current_track_instrument_short_name_helper()] 165 | 166 | def get_midi_cc_controls_for_current_track_and_section(self): 167 | section, _ = self.get_currently_selected_midi_cc_section_and_page() 168 | return [control for control in self.instrument_midi_control_ccs.get(self.get_current_track_instrument_short_name_helper(), []) if control.section == section] 169 | 170 | def get_midi_cc_controls_for_current_track_section_and_page(self): 171 | all_section_controls = self.get_midi_cc_controls_for_current_track_and_section() 172 | _, page = self.get_currently_selected_midi_cc_section_and_page() 173 | try: 174 | return all_section_controls[page * 8:(page+1) * 8] 175 | except IndexError: 176 | return [] 177 | 178 | def update_current_section_page(self, new_section=None, new_page=None): 179 | current_section, current_page = self.get_currently_selected_midi_cc_section_and_page() 180 | result = [current_section, current_page] 181 | if new_section is not None: 182 | result[0] = new_section 183 | if new_page is not None: 184 | result[1] = new_page 185 | self.current_selected_section_and_page[self.get_current_track_instrument_short_name_helper()] = result 186 | self.active_midi_control_ccs = self.get_midi_cc_controls_for_current_track_section_and_page() 187 | self.app.buttons_need_update = True 188 | 189 | def get_should_show_midi_cc_next_prev_pages_for_section(self): 190 | all_section_controls = self.get_midi_cc_controls_for_current_track_and_section() 191 | _, page = self.get_currently_selected_midi_cc_section_and_page() 192 | show_prev = False 193 | if page > 0: 194 | show_prev = True 195 | show_next = False 196 | if (page + 1) * 8 < len(all_section_controls): 197 | show_next = True 198 | return show_prev, show_next 199 | 200 | def new_track_selected(self): 201 | self.active_midi_control_ccs = self.get_midi_cc_controls_for_current_track_section_and_page() 202 | 203 | def activate(self): 204 | self.update_buttons() 205 | 206 | def deactivate(self): 207 | for button_name in self.midi_cc_button_names + [push2_python.constants.BUTTON_PAGE_LEFT, push2_python.constants.BUTTON_PAGE_RIGHT]: 208 | self.push.buttons.set_button_color(button_name, definitions.BLACK) 209 | 210 | def update_buttons(self): 211 | 212 | n_midi_cc_sections = len(self.get_current_track_midi_cc_sections()) 213 | for count, name in enumerate(self.midi_cc_button_names): 214 | if count < n_midi_cc_sections: 215 | self.push.buttons.set_button_color(name, definitions.WHITE) 216 | else: 217 | self.push.buttons.set_button_color(name, definitions.BLACK) 218 | 219 | show_prev, show_next = self.get_should_show_midi_cc_next_prev_pages_for_section() 220 | if show_prev: 221 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_PAGE_LEFT, definitions.WHITE) 222 | else: 223 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_PAGE_LEFT, definitions.BLACK) 224 | if show_next: 225 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_PAGE_RIGHT, definitions.WHITE) 226 | else: 227 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_PAGE_RIGHT, definitions.BLACK) 228 | 229 | def update_display(self, ctx, w, h): 230 | 231 | if not self.app.is_mode_active(self.app.settings_mode): 232 | # If settings mode is active, don't draw the upper parts of the screen because settings page will 233 | # "cover them" 234 | 235 | # Draw MIDI CCs section names 236 | section_names = self.get_current_track_midi_cc_sections()[0:8] 237 | if section_names: 238 | height = 20 239 | for i, section_name in enumerate(section_names): 240 | show_text(ctx, i, 0, section_name, background_color=definitions.RED) 241 | 242 | is_selected = False 243 | selected_section, _ = self.get_currently_selected_midi_cc_section_and_page() 244 | if selected_section == section_name: 245 | is_selected = True 246 | 247 | current_track_color = self.get_current_track_color_helper() 248 | if is_selected: 249 | background_color = current_track_color 250 | font_color = definitions.BLACK 251 | else: 252 | background_color = definitions.BLACK 253 | font_color = current_track_color 254 | show_text(ctx, i, 0, section_name, height=height, 255 | font_color=font_color, background_color=background_color) 256 | 257 | # Draw MIDI CC controls 258 | if self.active_midi_control_ccs: 259 | for i in range(0, min(len(self.active_midi_control_ccs), 8)): 260 | try: 261 | self.active_midi_control_ccs[i].draw(ctx, i) 262 | except IndexError: 263 | continue 264 | 265 | 266 | def on_button_pressed(self, button_name): 267 | if button_name in self.midi_cc_button_names: 268 | current_track_sections = self.get_current_track_midi_cc_sections() 269 | n_sections = len(current_track_sections) 270 | idx = self.midi_cc_button_names.index(button_name) 271 | if idx < n_sections: 272 | new_section = current_track_sections[idx] 273 | self.update_current_section_page(new_section=new_section, new_page=0) 274 | return True 275 | 276 | elif button_name in [push2_python.constants.BUTTON_PAGE_LEFT, push2_python.constants.BUTTON_PAGE_RIGHT]: 277 | show_prev, show_next = self.get_should_show_midi_cc_next_prev_pages_for_section() 278 | _, current_page = self.get_currently_selected_midi_cc_section_and_page() 279 | if button_name == push2_python.constants.BUTTON_PAGE_LEFT and show_prev: 280 | self.update_current_section_page(new_page=current_page - 1) 281 | elif button_name == push2_python.constants.BUTTON_PAGE_RIGHT and show_next: 282 | self.update_current_section_page(new_page=current_page + 1) 283 | return True 284 | 285 | 286 | def on_encoder_rotated(self, encoder_name, increment): 287 | try: 288 | encoder_num = [ 289 | push2_python.constants.ENCODER_TRACK1_ENCODER, 290 | push2_python.constants.ENCODER_TRACK2_ENCODER, 291 | push2_python.constants.ENCODER_TRACK3_ENCODER, 292 | push2_python.constants.ENCODER_TRACK4_ENCODER, 293 | push2_python.constants.ENCODER_TRACK5_ENCODER, 294 | push2_python.constants.ENCODER_TRACK6_ENCODER, 295 | push2_python.constants.ENCODER_TRACK7_ENCODER, 296 | push2_python.constants.ENCODER_TRACK8_ENCODER, 297 | ].index(encoder_name) 298 | if self.active_midi_control_ccs: 299 | self.active_midi_control_ccs[encoder_num].update_value(increment) 300 | except ValueError: 301 | pass # Encoder not in list 302 | return True # Always return True because encoder should not be used in any other mode if this is first active 303 | -------------------------------------------------------------------------------- /preset_selection_mode.py: -------------------------------------------------------------------------------- 1 | import definitions 2 | import mido 3 | import push2_python 4 | import time 5 | import os 6 | import json 7 | 8 | from display_utils import show_notification 9 | 10 | 11 | class PresetSelectionMode(definitions.PyshaMode): 12 | 13 | xor_group = 'pads' 14 | 15 | favourtie_presets = {} 16 | favourtie_presets_filename = 'favourite_presets.json' 17 | pad_pressing_states = {} 18 | pad_quick_press_time = 0.400 19 | current_page = 0 20 | 21 | def initialize(self, settings=None): 22 | if os.path.exists(self.favourtie_presets_filename): 23 | self.favourtie_presets = json.load(open(self.favourtie_presets_filename)) 24 | 25 | def activate(self): 26 | self.current_page = 0 27 | self.update_buttons() 28 | self.update_pads() 29 | 30 | def new_track_selected(self): 31 | self.current_page = 0 32 | self.app.pads_need_update = True 33 | self.app.buttons_need_update = True 34 | 35 | def add_favourite_preset(self, preset_number, bank_number): 36 | instrument_short_name = self.app.track_selection_mode.get_current_track_instrument_short_name() 37 | if instrument_short_name not in self.favourtie_presets: 38 | self.favourtie_presets[instrument_short_name] = [] 39 | self.favourtie_presets[instrument_short_name].append((preset_number, bank_number)) 40 | json.dump(self.favourtie_presets, open(self.favourtie_presets_filename, 'w')) # Save to file 41 | 42 | def remove_favourite_preset(self, preset_number, bank_number): 43 | instrument_short_name = self.app.track_selection_mode.get_current_track_instrument_short_name() 44 | if instrument_short_name in self.favourtie_presets: 45 | self.favourtie_presets[instrument_short_name] = \ 46 | [(fp_preset_number, fp_bank_number) for fp_preset_number, fp_bank_number in self.favourtie_presets[instrument_short_name] 47 | if preset_number != fp_preset_number or bank_number != fp_bank_number] 48 | json.dump(self.favourtie_presets, open(self.favourtie_presets_filename, 'w')) # Save to file 49 | 50 | def preset_num_in_favourites(self, preset_number, bank_number): 51 | instrument_short_name = self.app.track_selection_mode.get_current_track_instrument_short_name() 52 | if instrument_short_name not in self.favourtie_presets: 53 | return False 54 | for fp_preset_number, fp_bank_number in self.favourtie_presets[instrument_short_name]: 55 | if preset_number == fp_preset_number and bank_number == fp_bank_number: 56 | return True 57 | return False 58 | 59 | def get_current_page(self): 60 | # Returns the current page of presets being displayed in the pad grid 61 | # page 0 = bank 0, presets 0-63 62 | # page 1 = bank 0, presets 64-127 63 | # page 2 = bank 1, presets 0-63 64 | # page 3 = bank 1, presets 64-127 65 | # ... 66 | # The number of total available pages depends on the synth. 67 | return self.current_page 68 | 69 | def get_num_banks(self): 70 | # Returns the number of available banks of the selected instrument 71 | return self.app.track_selection_mode.get_current_track_info()['n_banks'] 72 | 73 | def get_bank_names(self): 74 | # Returns list of bank names 75 | return self.app.track_selection_mode.get_current_track_info()['bank_names'] 76 | 77 | def get_num_pages(self): 78 | # Returns the number of available preset pages per instrument (2 per bank) 79 | return self.get_num_banks() * 2 80 | 81 | def next_page(self): 82 | if self.current_page < self.get_num_pages() - 1: 83 | self.current_page += 1 84 | else: 85 | self.current_page = self.get_num_pages() - 1 86 | self.app.pads_need_update = True 87 | self.app.buttons_need_update = True 88 | self.notify_status_in_display() 89 | 90 | def prev_page(self): 91 | if self.current_page > 0: 92 | self.current_page -= 1 93 | else: 94 | self.current_page = 0 95 | self.app.pads_need_update = True 96 | self.app.buttons_need_update = True 97 | self.notify_status_in_display() 98 | 99 | def has_prev_next_pages(self): 100 | has_next = False 101 | has_prev = False 102 | if self.get_current_page() < self.get_num_pages() - 1: 103 | has_next = True 104 | if self.get_current_page() > 0: 105 | has_prev = True 106 | return (has_prev, has_next) 107 | 108 | def pad_ij_to_bank_and_preset_num(self, pad_ij): 109 | preset_num = (self.get_current_page() % 2) * 64 + pad_ij[0] * 8 + pad_ij[1] 110 | bank_num = self.get_current_page() // 2 111 | return (preset_num, bank_num) 112 | 113 | def send_select_new_preset(self, preset_num): 114 | msg = mido.Message('program_change', program=preset_num) # Should this be 1-indexed? 115 | self.app.send_midi(msg) 116 | 117 | def send_select_new_bank(self, bank_num): 118 | # If synth only has 1 bank, don't send bank change messages 119 | if self.get_num_banks() > 1: 120 | msg = mido.Message('control_change', control=0, value=bank_num) # Should this be 1-indexed? 121 | self.app.send_midi(msg) 122 | 123 | def notify_status_in_display(self): 124 | bank_number = self.get_current_page() // 2 + 1 125 | bank_names = self.get_bank_names() 126 | if bank_names is not None: 127 | bank_name = bank_names[bank_number - 1] 128 | else: 129 | bank_name = bank_number 130 | self.app.add_display_notification("Preset selection: bank {0}, presets {1}".format( 131 | bank_name, 132 | '1-64' if self.get_current_page() % 2 == 0 else '65-128' 133 | )) 134 | 135 | def activate(self): 136 | self.update_pads() 137 | self.notify_status_in_display() 138 | 139 | def deactivate(self): 140 | self.app.push.pads.set_all_pads_to_color(color=definitions.BLACK) 141 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_LEFT, definitions.BLACK) 142 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_RIGHT, definitions.BLACK) 143 | 144 | def update_buttons(self): 145 | show_prev, show_next = self.has_prev_next_pages() 146 | if show_prev: 147 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_LEFT, definitions.WHITE) 148 | else: 149 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_LEFT, definitions.BLACK) 150 | if show_next: 151 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_RIGHT, definitions.WHITE) 152 | else: 153 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_RIGHT, definitions.BLACK) 154 | 155 | def update_pads(self): 156 | instrument_short_name = self.app.track_selection_mode.get_current_track_instrument_short_name() 157 | track_color = self.app.track_selection_mode.get_current_track_color() 158 | color_matrix = [] 159 | for i in range(0, 8): 160 | row_colors = [] 161 | for j in range(0, 8): 162 | cell_color = track_color 163 | preset_num, bank_num = self.pad_ij_to_bank_and_preset_num((i, j)) 164 | if not self.preset_num_in_favourites(preset_num, bank_num): 165 | cell_color = f'{cell_color}_darker2' # If preset not in favourites, use a darker version of the track color 166 | row_colors.append(cell_color) 167 | color_matrix.append(row_colors) 168 | self.push.pads.set_pads_color(color_matrix) 169 | 170 | def on_pad_pressed(self, pad_n, pad_ij, velocity): 171 | self.pad_pressing_states[pad_n] = time.time() # Store time at which pad_n was pressed 172 | self.push.pads.set_pad_color(pad_ij, color=definitions.GREEN) 173 | return True # Prevent other modes to get this event 174 | 175 | def on_pad_released(self, pad_n, pad_ij, velocity): 176 | pressing_time = self.pad_pressing_states.get(pad_n, None) 177 | is_long_press = False 178 | if pressing_time is None: 179 | # Consider quick press (this should not happen as self.pad_pressing_states[pad_n] should have been set before) 180 | pass 181 | else: 182 | if time.time() - pressing_time > self.pad_quick_press_time: 183 | # Consider this is a long press 184 | is_long_press = True 185 | self.pad_pressing_states[pad_n] = None # Reset pressing time to none 186 | 187 | preset_num, bank_num = self.pad_ij_to_bank_and_preset_num(pad_ij) 188 | 189 | if is_long_press: 190 | # Add/remove preset to favourites, don't send any MIDI 191 | if not self.preset_num_in_favourites(preset_num, bank_num): 192 | self.add_favourite_preset(preset_num, bank_num) 193 | else: 194 | self.remove_favourite_preset(preset_num, bank_num) 195 | else: 196 | # Send midi message to select the bank and preset preset 197 | self.send_select_new_bank(bank_num) 198 | self.send_select_new_preset(preset_num) 199 | bank_names = self.get_bank_names() 200 | if bank_names is not None: 201 | bank_name = bank_names[bank_num] 202 | else: 203 | bank_name = bank_num + 1 204 | self.app.add_display_notification("Selected bank {0}, preset {1}".format( 205 | bank_name, # Show 1-indexed value 206 | preset_num + 1 # Show 1-indexed value 207 | )) 208 | 209 | self.app.pads_need_update = True 210 | return True # Prevent other modes to get this event 211 | 212 | def on_button_pressed(self, button_name): 213 | if button_name in [push2_python.constants.BUTTON_LEFT, push2_python.constants.BUTTON_RIGHT]: 214 | show_prev, show_next = self.has_prev_next_pages() 215 | if button_name == push2_python.constants.BUTTON_LEFT and show_prev: 216 | self.prev_page() 217 | elif button_name == push2_python.constants.BUTTON_RIGHT and show_next: 218 | self.next_page() 219 | return True 220 | -------------------------------------------------------------------------------- /pyramid_track_triggering_mode.py: -------------------------------------------------------------------------------- 1 | import definitions 2 | import mido 3 | import push2_python 4 | import time 5 | import math 6 | import os 7 | import json 8 | 9 | 10 | class PyramidTrackState(object): 11 | 12 | track_num = 0 13 | has_content = False 14 | is_playing = False 15 | 16 | def __init__(self, track_num=0): 17 | self.track_num = track_num 18 | 19 | 20 | class PyramidTrackTriggeringMode(definitions.PyshaMode): 21 | 22 | xor_group = 'pads' 23 | 24 | scene_trigger_buttons = [ 25 | push2_python.constants.BUTTON_1_32T, 26 | push2_python.constants.BUTTON_1_32, 27 | push2_python.constants.BUTTON_1_16T, 28 | push2_python.constants.BUTTON_1_16, 29 | push2_python.constants.BUTTON_1_8T, 30 | push2_python.constants.BUTTON_1_8, 31 | push2_python.constants.BUTTON_1_4T, 32 | push2_python.constants.BUTTON_1_4 33 | ] 34 | 35 | pyramidi_channel = 15 36 | 37 | track_states = [] 38 | 39 | pad_pressing_states = {} 40 | pad_quick_press_time = 0.400 41 | 42 | track_selection_modifier_button_being_pressed = False 43 | track_selection_modifier_button = push2_python.constants.BUTTON_MASTER 44 | 45 | def initialize(self, settings=None): 46 | self.pyramidi_channel = self.app.track_selection_mode.pyramidi_channel # Note TrackSelectionMode needs to have been initialized before PyramidTrackTriggeringMode 47 | self.create_tracks() 48 | 49 | def create_tracks(self): 50 | for i in range(0, 64): 51 | self.track_states.append(PyramidTrackState(track_num=i)) 52 | 53 | def track_is_playing(self, track_num): 54 | return self.track_states[track_num].is_playing 55 | 56 | def track_has_content(self, track_num): 57 | return self.track_states[track_num].has_content 58 | 59 | def set_track_is_playing(self, track_num, value, send_to_pyramid=True): 60 | self.track_states[track_num].is_playing = value 61 | if send_to_pyramid: 62 | if value == True: 63 | self.send_unmute_track_to_pyramid(track_num) 64 | else: 65 | self.send_mute_track_to_pyramid(track_num) 66 | 67 | def set_track_has_content(self, track_num, value): 68 | self.track_states[track_num].has_content = value 69 | 70 | def set_pyramidi_channel(self, channel, wrap=False): 71 | self.pyramidi_channel = channel 72 | if self.pyramidi_channel < 0: 73 | self.pyramidi_channel = 0 if not wrap else 15 74 | elif self.pyramidi_channel > 15: 75 | self.pyramidi_channel = 15 if not wrap else 0 76 | 77 | def pad_ij_to_track_num(self, pad_ij): 78 | return pad_ij[0] * 8 + pad_ij[1] 79 | 80 | def send_mute_track_to_pyramid(self, track_num): 81 | # Follows pyramidi specification (Pyramid configured to receive on ch 16) 82 | msg = mido.Message('control_change', control=track_num + 1, value=0, channel=self.pyramidi_channel) 83 | self.app.send_midi_to_pyramid(msg) 84 | 85 | def send_unmute_track_to_pyramid(self, track_num): 86 | # Follows pyramidi specification (Pyramid configured to receive on ch 16) 87 | msg = mido.Message('control_change', control=track_num + 1, value=1, channel=self.pyramidi_channel) 88 | self.app.send_midi_to_pyramid(msg) 89 | 90 | def activate(self): 91 | self.pad_pressing_states = {} 92 | self.track_selection_modifier_button_being_pressed = False 93 | self.update_buttons() 94 | self.update_pads() 95 | 96 | def new_track_selected(self): 97 | self.pad_pressing_states = {} 98 | self.track_selection_modifier_button_being_pressed = False 99 | self.app.pads_need_update = True 100 | self.app.buttons_need_update = True 101 | 102 | def deactivate(self): 103 | for button_name in self.scene_trigger_buttons: 104 | self.push.buttons.set_button_color(button_name, definitions.BLACK) 105 | self.push.buttons.set_button_color(self.track_selection_modifier_button, definitions.BLACK) 106 | self.app.push.pads.set_all_pads_to_color(color=definitions.BLACK) 107 | 108 | def update_buttons(self): 109 | for button_name in self.scene_trigger_buttons: 110 | self.push.buttons.set_button_color(button_name, definitions.WHITE) 111 | if not self.track_selection_modifier_button_being_pressed: 112 | self.push.buttons.set_button_color(self.track_selection_modifier_button, definitions.OFF_BTN_COLOR) 113 | else: 114 | self.push.buttons.set_button_color(self.track_selection_modifier_button, definitions.BLACK) 115 | self.push.buttons.set_button_color(self.track_selection_modifier_button, definitions.WHITE, animation=definitions.DEFAULT_ANIMATION) 116 | 117 | def update_pads(self): 118 | # Update pads according to track state 119 | color_matrix = [] 120 | for i in range(0, 8): 121 | row_colors = [] 122 | for j in range(0, 8): 123 | track_num = self.pad_ij_to_track_num((i, j)) 124 | track_color = self.app.track_selection_mode.get_track_color(track_num) # Track color 125 | cell_color = track_color + '_darker2' # Choose super darker version of track color 126 | if self.track_has_content(track_num): 127 | cell_color = track_color + '_darker1' # Choose darker version of track color 128 | if self.track_is_playing(track_num): 129 | cell_color = track_color 130 | row_colors.append(cell_color) 131 | color_matrix.append(row_colors) 132 | self.push.pads.set_pads_color(color_matrix) 133 | 134 | def on_button_pressed(self, button_name): 135 | if button_name in self.scene_trigger_buttons: 136 | triggered_scene_row = self.scene_trigger_buttons.index(button_name) 137 | # Unmute all tracks in that row, mute all tracks from other rows (only tracks that have content) 138 | for i in range(0, 8): 139 | for j in range(0, 8): 140 | track_num = self.pad_ij_to_track_num((i, j)) 141 | # If track in selected row 142 | # # TODO: check that indexing is correct 143 | if i == triggered_scene_row: 144 | if self.track_has_content(track_num): 145 | self.set_track_is_playing(track_num, True) 146 | else: 147 | if self.track_has_content(track_num): 148 | self.set_track_is_playing(track_num, False) 149 | self.app.pads_need_update = True 150 | 151 | return True # Prevent other modes to get this event 152 | 153 | elif button_name == self.track_selection_modifier_button: 154 | self.track_selection_modifier_button_being_pressed = True 155 | return True # Prevent other modes to get this event 156 | 157 | def on_button_released(self, button_name): 158 | if button_name == self.track_selection_modifier_button: 159 | self.track_selection_modifier_button_being_pressed = False 160 | return True # Prevent other modes to get this event 161 | 162 | def on_pad_pressed(self, pad_n, pad_ij, velocity): 163 | if not self.track_selection_modifier_button_being_pressed: 164 | self.pad_pressing_states[pad_n] = time.time() # Store time at which pad_n was pressed 165 | self.push.pads.set_pad_color(pad_ij, color=definitions.GREEN) 166 | return True # Prevent other modes to get this event 167 | else: 168 | # If a pad is pressed while the modifier key is also pressed, 169 | # we select the corresponding track. This will trigger exiting 170 | # the PyramidTrackTriggering mode 171 | track_num = self.pad_ij_to_track_num(pad_ij) 172 | self.app.track_selection_mode.select_track(track_num) 173 | return True # Prevent other modes to get this event 174 | 175 | def on_pad_released(self, pad_n, pad_ij, velocity): 176 | pressing_time = self.pad_pressing_states.get(pad_n, None) 177 | is_long_press = False 178 | if pressing_time is None: 179 | # Consider quick press (this should not happen as self.pad_pressing_states[pad_n] should have been set before) 180 | pass 181 | else: 182 | if time.time() - pressing_time > self.pad_quick_press_time: 183 | # Consider this is a long press 184 | is_long_press = True 185 | self.pad_pressing_states[pad_n] = None # Reset pressing time to none 186 | 187 | track_num = self.pad_ij_to_track_num(pad_ij) 188 | 189 | if is_long_press: 190 | # Long press 191 | # - if track has no content: mark it as having content 192 | # - if track has content: mark it as having no content 193 | self.set_track_has_content(track_num, not self.track_has_content(track_num)) 194 | if self.track_is_playing(track_num): 195 | self.set_track_is_playing(track_num, False) 196 | 197 | else: 198 | # Short press 199 | # - if track has no content: mark it as having content 200 | # - if track has content: toggle mute/unmute 201 | if not self.track_has_content(track_num): 202 | self.set_track_has_content(track_num, True) 203 | else: 204 | if self.track_is_playing(track_num): 205 | self.set_track_is_playing(track_num, False) 206 | else: 207 | self.set_track_is_playing(track_num, True) 208 | 209 | self.app.pads_need_update = True 210 | return True # Prevent other modes to get this event 211 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | git+https://github.com/ffont/push2-python 2 | pycairo 3 | psutil -------------------------------------------------------------------------------- /rhythmic_mode.py: -------------------------------------------------------------------------------- 1 | import definitions 2 | import push2_python.constants 3 | 4 | from melodic_mode import MelodicMode 5 | 6 | 7 | class RhythmicMode(MelodicMode): 8 | 9 | rhythmic_notes_matrix = [ 10 | [64, 65, 66, 67, 96, 97, 98, 99], 11 | [60, 61, 62, 63, 92, 93, 94, 95], 12 | [56, 57, 58, 59, 88, 89, 90, 91], 13 | [52, 53, 54, 55, 84, 85, 86, 87], 14 | [48, 49, 50, 51, 80, 81, 82, 83], 15 | [44, 45, 46, 47, 76, 77, 78, 79], 16 | [40, 41, 42, 43, 72, 73, 74, 75], 17 | [36, 37, 38, 39, 68, 69, 70, 71] 18 | ] 19 | 20 | def get_settings_to_save(self): 21 | return {} 22 | 23 | def pad_ij_to_midi_note(self, pad_ij): 24 | return self.rhythmic_notes_matrix[pad_ij[0]][pad_ij[1]] 25 | 26 | def update_octave_buttons(self): 27 | # Rhythmic does not have octave buttons 28 | pass 29 | 30 | def update_pads(self): 31 | color_matrix = [] 32 | for i in range(0, 8): 33 | row_colors = [] 34 | for j in range(0, 8): 35 | corresponding_midi_note = self.pad_ij_to_midi_note([i, j]) 36 | cell_color = definitions.BLACK 37 | if i >= 4 and j < 4: 38 | # This is the main 4x4 grid 39 | cell_color = self.app.track_selection_mode.get_current_track_color() 40 | elif i >= 4 and j >= 4: 41 | cell_color = definitions.GRAY_LIGHT 42 | elif i < 4 and j < 4: 43 | cell_color = definitions.GRAY_LIGHT 44 | elif i < 4 and j >= 4: 45 | cell_color = definitions.GRAY_LIGHT 46 | if self.is_midi_note_being_played(corresponding_midi_note): 47 | cell_color = definitions.NOTE_ON_COLOR 48 | 49 | row_colors.append(cell_color) 50 | color_matrix.append(row_colors) 51 | 52 | self.push.pads.set_pads_color(color_matrix) 53 | 54 | def on_button_pressed(self, button_name): 55 | if button_name == push2_python.constants.BUTTON_OCTAVE_UP or button_name == push2_python.constants.BUTTON_OCTAVE_DOWN: 56 | # Don't react to octave up/down buttons as these are not used in rhythm mode 57 | pass 58 | else: 59 | # For the other buttons, refer to the base class 60 | super().on_button_pressed(button_name) 61 | -------------------------------------------------------------------------------- /settings_mode.py: -------------------------------------------------------------------------------- 1 | import definitions 2 | import push2_python.constants 3 | import time 4 | import os 5 | import sys 6 | import psutil 7 | import threading 8 | import subprocess 9 | 10 | from display_utils import show_title, show_value, draw_text_at 11 | 12 | 13 | class SettingsMode(definitions.PyshaMode): 14 | 15 | # Pad settings 16 | # - Root note 17 | # - Aftertouch mode 18 | # - Velocity curve 19 | # - Channel aftertouch range 20 | 21 | # MIDI settings 22 | # - Midi device IN 23 | # - Midi channel IN 24 | # - Midi device OUT 25 | # - Midi channel OUT 26 | # - Pyramidi channel 27 | # - Notes Midi 28 | # - Rerun MIDI initial configuration 29 | 30 | # About panel 31 | # - definitions.VERSION info 32 | # - Save current settings 33 | # - FPS 34 | 35 | current_page = 0 36 | n_pages = 3 37 | encoders_state = {} 38 | is_running_sw_update = False 39 | 40 | def move_to_next_page(self): 41 | self.app.buttons_need_update = True 42 | self.current_page += 1 43 | if self.current_page >= self.n_pages: 44 | self.current_page = 0 45 | return True # Return true because page rotation finished 46 | return False 47 | 48 | def initialize(self, settings=None): 49 | current_time = time.time() 50 | for encoder_name in self.push.encoders.available_names: 51 | self.encoders_state[encoder_name] = { 52 | 'last_message_received': current_time, 53 | } 54 | 55 | def activate(self): 56 | self.current_page = 0 57 | self.update_buttons() 58 | 59 | def deactivate(self): 60 | self.set_all_upper_row_buttons_off() 61 | 62 | def check_for_delayed_actions(self): 63 | current_time = time.time() 64 | 65 | if self.app.midi_in_tmp_device_idx is not None: 66 | # Means we are in the process of changing the MIDI in device 67 | if current_time - self.encoders_state[push2_python.constants.ENCODER_TRACK1_ENCODER]['last_message_received'] > definitions.DELAYED_ACTIONS_APPLY_TIME: 68 | self.app.set_midi_in_device_by_index(self.app.midi_in_tmp_device_idx) 69 | self.app.midi_in_tmp_device_idx = None 70 | 71 | if self.app.midi_out_tmp_device_idx is not None: 72 | # Means we are in the process of changing the MIDI out device 73 | if current_time - self.encoders_state[push2_python.constants.ENCODER_TRACK3_ENCODER]['last_message_received'] > definitions.DELAYED_ACTIONS_APPLY_TIME: 74 | self.app.set_midi_out_device_by_index(self.app.midi_out_tmp_device_idx) 75 | self.app.midi_out_tmp_device_idx = None 76 | 77 | if self.app.notes_midi_in_tmp_device_idx is not None: 78 | # Means we are in the process of changing the notes MIDI in device 79 | if current_time - self.encoders_state[push2_python.constants.ENCODER_TRACK6_ENCODER]['last_message_received'] > definitions.DELAYED_ACTIONS_APPLY_TIME: 80 | self.app.set_notes_midi_in_device_by_index(self.app.notes_midi_in_tmp_device_idx) 81 | self.app.notes_midi_in_tmp_device_idx = None 82 | 83 | def set_all_upper_row_buttons_off(self): 84 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_UPPER_ROW_1, definitions.OFF_BTN_COLOR) 85 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_UPPER_ROW_2, definitions.OFF_BTN_COLOR) 86 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_UPPER_ROW_3, definitions.OFF_BTN_COLOR) 87 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_UPPER_ROW_4, definitions.OFF_BTN_COLOR) 88 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_UPPER_ROW_5, definitions.OFF_BTN_COLOR) 89 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_UPPER_ROW_6, definitions.OFF_BTN_COLOR) 90 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_UPPER_ROW_7, definitions.OFF_BTN_COLOR) 91 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_UPPER_ROW_8, definitions.OFF_BTN_COLOR) 92 | 93 | def update_buttons(self): 94 | if self.current_page == 0: # Performance settings 95 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_UPPER_ROW_1, definitions.WHITE) 96 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_UPPER_ROW_2, definitions.WHITE) 97 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_UPPER_ROW_3, definitions.OFF_BTN_COLOR) 98 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_UPPER_ROW_4, definitions.OFF_BTN_COLOR) 99 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_UPPER_ROW_5, definitions.OFF_BTN_COLOR) 100 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_UPPER_ROW_6, definitions.OFF_BTN_COLOR) 101 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_UPPER_ROW_7, definitions.OFF_BTN_COLOR) 102 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_UPPER_ROW_8, definitions.OFF_BTN_COLOR) 103 | 104 | elif self.current_page == 1: # MIDI settings 105 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_UPPER_ROW_1, definitions.WHITE) 106 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_UPPER_ROW_2, definitions.WHITE) 107 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_UPPER_ROW_3, definitions.WHITE) 108 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_UPPER_ROW_4, definitions.WHITE) 109 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_UPPER_ROW_5, definitions.WHITE) 110 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_UPPER_ROW_6, definitions.WHITE) 111 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_UPPER_ROW_7, definitions.BLACK) 112 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_UPPER_ROW_7, definitions.GREEN, animation=definitions.DEFAULT_ANIMATION) 113 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_UPPER_ROW_8, definitions.OFF_BTN_COLOR) 114 | 115 | elif self.current_page == 2: # About 116 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_UPPER_ROW_1, definitions.GREEN) 117 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_UPPER_ROW_2, definitions.OFF_BTN_COLOR) 118 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_UPPER_ROW_3, definitions.BLACK) 119 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_UPPER_ROW_3, definitions.RED, animation=definitions.DEFAULT_ANIMATION) 120 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_UPPER_ROW_4, definitions.OFF_BTN_COLOR) 121 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_UPPER_ROW_5, definitions.OFF_BTN_COLOR) 122 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_UPPER_ROW_6, definitions.OFF_BTN_COLOR) 123 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_UPPER_ROW_7, definitions.OFF_BTN_COLOR) 124 | self.push.buttons.set_button_color(push2_python.constants.BUTTON_UPPER_ROW_8, definitions.OFF_BTN_COLOR) 125 | 126 | def update_display(self, ctx, w, h): 127 | 128 | # Divide display in 8 parts to show different settings 129 | part_w = w // 8 130 | part_h = h 131 | 132 | # Draw labels and values 133 | for i in range(0, 8): 134 | part_x = i * part_w 135 | part_y = 0 136 | 137 | ctx.set_source_rgb(0, 0, 0) # Draw black background 138 | ctx.rectangle(part_x - 3, part_y, w, h) # do x -3 to add some margin between parts 139 | ctx.fill() 140 | 141 | color = [1.0, 1.0, 1.0] 142 | 143 | if self.current_page == 0: # Performance settings 144 | if i == 0: # Root note 145 | if not self.app.is_mode_active(self.app.melodic_mode): 146 | color = definitions.get_color_rgb_float(definitions.FONT_COLOR_DISABLED) 147 | show_title(ctx, part_x, h, 'ROOT NOTE') 148 | show_value(ctx, part_x, h, "{0} ({1})".format(self.app.melodic_mode.note_number_to_name( 149 | self.app.melodic_mode.root_midi_note), self.app.melodic_mode.root_midi_note), color) 150 | 151 | elif i == 1: # Poly AT/channel AT 152 | show_title(ctx, part_x, h, 'AFTERTOUCH') 153 | show_value(ctx, part_x, h, 'polyAT' if self.app.melodic_mode.use_poly_at else 'channel', color) 154 | 155 | elif i == 2: # Channel AT range start 156 | if self.app.melodic_mode.last_time_at_params_edited is not None: 157 | color = definitions.get_color_rgb_float(definitions.FONT_COLOR_DELAYED_ACTIONS) 158 | show_title(ctx, part_x, h, 'cAT START') 159 | show_value(ctx, part_x, h, self.app.melodic_mode.channel_at_range_start, color) 160 | 161 | elif i == 3: # Channel AT range end 162 | if self.app.melodic_mode.last_time_at_params_edited is not None: 163 | color = definitions.get_color_rgb_float(definitions.FONT_COLOR_DELAYED_ACTIONS) 164 | show_title(ctx, part_x, h, 'cAT END') 165 | show_value(ctx, part_x, h, self.app.melodic_mode.channel_at_range_end, color) 166 | 167 | elif i == 4: # Poly AT range 168 | if self.app.melodic_mode.last_time_at_params_edited is not None: 169 | color = definitions.get_color_rgb_float(definitions.FONT_COLOR_DELAYED_ACTIONS) 170 | show_title(ctx, part_x, h, 'pAT RANGE') 171 | show_value(ctx, part_x, h, self.app.melodic_mode.poly_at_max_range, color) 172 | 173 | elif i == 5: # Poly AT curve 174 | if self.app.melodic_mode.last_time_at_params_edited is not None: 175 | color = definitions.get_color_rgb_float(definitions.FONT_COLOR_DELAYED_ACTIONS) 176 | show_title(ctx, part_x, h, 'pAT CURVE') 177 | show_value(ctx, part_x, h, self.app.melodic_mode.poly_at_curve_bending, color) 178 | 179 | elif self.current_page == 1: # MIDI settings 180 | if i == 0: # MIDI in device 181 | if self.app.midi_in_tmp_device_idx is not None: 182 | color = definitions.get_color_rgb_float(definitions.FONT_COLOR_DELAYED_ACTIONS) 183 | if self.app.midi_in_tmp_device_idx < 0: 184 | name = "None" 185 | else: 186 | name = "{0} {1}".format(self.app.midi_in_tmp_device_idx + 1, self.app.available_midi_in_device_names[self.app.midi_in_tmp_device_idx]) 187 | else: 188 | if self.app.midi_in is not None: 189 | name = "{0} {1}".format(self.app.available_midi_in_device_names.index(self.app.midi_in.name) + 1, self.app.midi_in.name) 190 | else: 191 | color = definitions.get_color_rgb_float(definitions.FONT_COLOR_DISABLED) 192 | name = "None" 193 | show_title(ctx, part_x, h, 'IN DEVICE') 194 | show_value(ctx, part_x, h, name, color) 195 | 196 | elif i == 1: # MIDI in channel 197 | if self.app.midi_in is None: 198 | color = definitions.get_color_rgb_float(definitions.FONT_COLOR_DISABLED) 199 | show_title(ctx, part_x, h, 'IN CH') 200 | show_value(ctx, part_x, h, self.app.midi_in_channel + 1 if self.app.midi_in_channel > -1 else "All", color) 201 | 202 | elif i == 2: # MIDI out device 203 | if self.app.midi_out_tmp_device_idx is not None: 204 | color = definitions.get_color_rgb_float(definitions.FONT_COLOR_DELAYED_ACTIONS) 205 | if self.app.midi_out_tmp_device_idx < 0: 206 | name = "None" 207 | else: 208 | name = "{0} {1}".format(self.app.midi_out_tmp_device_idx + 1, self.app.available_midi_out_device_names[self.app.midi_out_tmp_device_idx]) 209 | else: 210 | if self.app.midi_out is not None: 211 | name = "{0} {1}".format(self.app.available_midi_out_device_names.index(self.app.midi_out.name) + 1, self.app.midi_out.name) 212 | else: 213 | color = definitions.get_color_rgb_float(definitions.FONT_COLOR_DISABLED) 214 | name = "None" 215 | show_title(ctx, part_x, h, 'OUT DEVICE') 216 | show_value(ctx, part_x, h, name, color) 217 | 218 | elif i == 3: # MIDI out channel 219 | if self.app.midi_out is None: 220 | color = definitions.get_color_rgb_float(definitions.FONT_COLOR_DISABLED) 221 | show_title(ctx, part_x, h, 'OUT CH') 222 | show_value(ctx, part_x, h, self.app.midi_out_channel + 1 if self.app.midi_out_channel >= 0 else 'TR', color) 223 | 224 | elif i == 4: # Pyramidi out channel 225 | show_title(ctx, part_x, h, 'PYRAMIDI CH') 226 | show_value(ctx, part_x, h, self.app.track_selection_mode.pyramidi_channel + 1, color) 227 | 228 | elif i == 5: # Notes MIDI in device 229 | if self.app.notes_midi_in_tmp_device_idx is not None: 230 | color = definitions.get_color_rgb_float(definitions.FONT_COLOR_DELAYED_ACTIONS) 231 | if self.app.notes_midi_in_tmp_device_idx < 0: 232 | name = "None" 233 | else: 234 | name = "{0} {1}".format(self.app.notes_midi_in_tmp_device_idx + 1, self.app.available_midi_in_device_names[self.app.notes_midi_in_tmp_device_idx]) 235 | else: 236 | if self.app.notes_midi_in is not None: 237 | name = "{0} {1}".format(self.app.available_midi_in_device_names.index(self.app.notes_midi_in.name) + 1, self.app.notes_midi_in.name) 238 | else: 239 | color = definitions.get_color_rgb_float(definitions.FONT_COLOR_DISABLED) 240 | name = "None" 241 | show_title(ctx, part_x, h, 'NOTES IN') 242 | show_value(ctx, part_x, h, name, color) 243 | 244 | elif i == 6: # Re-send MIDI connection established (to push, not MIDI in/out device) 245 | show_title(ctx, part_x, h, 'RESET MIDI') 246 | 247 | elif self.current_page == 2: # About 248 | if i == 0: # Save button 249 | show_title(ctx, part_x, h, 'SAVE') 250 | 251 | elif i ==1: # definitions.VERSION info 252 | show_title(ctx, part_x, h, 'VERSION') 253 | show_value(ctx, part_x, h, 'Pysha ' + definitions.VERSION, color) 254 | 255 | elif i == 2: # Software update 256 | show_title(ctx, part_x, h, 'SW UPDATE') 257 | if self.is_running_sw_update: 258 | show_value(ctx, part_x, h, 'Running... ', color) 259 | 260 | elif i == 3: # FPS indicator 261 | show_title(ctx, part_x, h, 'FPS') 262 | show_value(ctx, part_x, h, self.app.actual_frame_rate, color) 263 | 264 | # After drawing all labels and values, draw other stuff if required 265 | if self.current_page == 0: # Performance settings 266 | 267 | # Draw polyAT velocity curve 268 | ctx.set_source_rgb(0.6, 0.6, 0.6) 269 | ctx.set_line_width(1) 270 | data = self.app.melodic_mode.get_poly_at_curve() 271 | n = len(data) 272 | curve_x = 4 * part_w + 3 # Start x point of curve 273 | curve_y = part_h - 10 # Start y point of curve 274 | curve_height = 50 275 | curve_length = part_w * 4 - 6 276 | ctx.move_to(curve_x, curve_y) 277 | for i, value in enumerate(data): 278 | x = curve_x + i * curve_length/n 279 | y = curve_y - curve_height * value/127 280 | ctx.line_to(x, y) 281 | ctx.line_to(x, curve_y) 282 | ctx.fill() 283 | 284 | current_time = time.time() 285 | if current_time - self.app.melodic_mode.latest_channel_at_value[0] < 3 and not self.app.melodic_mode.use_poly_at: 286 | # Lastest channel AT value received less than 3 seconds ago 287 | draw_text_at(ctx, 3, part_h - 3, f'Latest cAT: {self.app.melodic_mode.latest_channel_at_value[1]}', font_size=20) 288 | if current_time - self.app.melodic_mode.latest_poly_at_value[0] < 3 and self.app.melodic_mode.use_poly_at: 289 | # Lastest channel AT value received less than 3 seconds ago 290 | draw_text_at(ctx, 3, part_h - 3, f'Latest pAT: {self.app.melodic_mode.latest_poly_at_value[1]}', font_size=20) 291 | if current_time - self.app.melodic_mode.latest_velocity_value[0] < 3: 292 | # Lastest note on velocity value received less than 3 seconds ago 293 | draw_text_at(ctx, 3, part_h - 26, f'Latest velocity: {self.app.melodic_mode.latest_velocity_value[1]}', font_size=20) 294 | 295 | 296 | def on_encoder_rotated(self, encoder_name, increment): 297 | 298 | self.encoders_state[encoder_name]['last_message_received'] = time.time() 299 | 300 | if self.current_page == 0: # Performance settings 301 | if encoder_name == push2_python.constants.ENCODER_TRACK1_ENCODER: 302 | self.app.melodic_mode.set_root_midi_note(self.app.melodic_mode.root_midi_note + increment) 303 | self.app.pads_need_update = True # Using async update method because we don't really need immediate response here 304 | 305 | elif encoder_name == push2_python.constants.ENCODER_TRACK2_ENCODER: 306 | if increment >= 3: # Only respond to "big" increments 307 | if not self.app.melodic_mode.use_poly_at: 308 | self.app.melodic_mode.use_poly_at = True 309 | self.app.push.pads.set_polyphonic_aftertouch() 310 | elif increment <= -3: 311 | if self.app.melodic_mode.use_poly_at: 312 | self.app.melodic_mode.use_poly_at = False 313 | self.app.push.pads.set_channel_aftertouch() 314 | self.app.melodic_mode.set_lumi_pressure_mode() 315 | 316 | elif encoder_name == push2_python.constants.ENCODER_TRACK3_ENCODER: 317 | self.app.melodic_mode.set_channel_at_range_start(self.app.melodic_mode.channel_at_range_start + increment) 318 | 319 | elif encoder_name == push2_python.constants.ENCODER_TRACK4_ENCODER: 320 | self.app.melodic_mode.set_channel_at_range_end(self.app.melodic_mode.channel_at_range_end + increment) 321 | 322 | elif encoder_name == push2_python.constants.ENCODER_TRACK5_ENCODER: 323 | self.app.melodic_mode.set_poly_at_max_range(self.app.melodic_mode.poly_at_max_range + increment) 324 | 325 | elif encoder_name == push2_python.constants.ENCODER_TRACK6_ENCODER: 326 | self.app.melodic_mode.set_poly_at_curve_bending(self.app.melodic_mode.poly_at_curve_bending + increment) 327 | 328 | elif self.current_page == 1: # MIDI settings 329 | if encoder_name == push2_python.constants.ENCODER_TRACK1_ENCODER: 330 | if self.app.midi_in_tmp_device_idx is None: 331 | if self.app.midi_in is not None: 332 | self.app.midi_in_tmp_device_idx = self.app.available_midi_in_device_names.index(self.app.midi_in.name) 333 | else: 334 | self.app.midi_in_tmp_device_idx = -1 335 | self.app.midi_in_tmp_device_idx += increment 336 | if self.app.midi_in_tmp_device_idx >= len(self.app.available_midi_in_device_names): 337 | self.app.midi_in_tmp_device_idx = len(self.app.available_midi_in_device_names) - 1 338 | elif self.app.midi_in_tmp_device_idx < -1: 339 | self.app.midi_in_tmp_device_idx = -1 # Will use -1 for "None" 340 | 341 | elif encoder_name == push2_python.constants.ENCODER_TRACK2_ENCODER: 342 | self.app.set_midi_in_channel(self.app.midi_in_channel + increment, wrap=False) 343 | 344 | elif encoder_name == push2_python.constants.ENCODER_TRACK3_ENCODER: 345 | if self.app.midi_out_tmp_device_idx is None: 346 | if self.app.midi_out is not None: 347 | self.app.midi_out_tmp_device_idx = self.app.available_midi_out_device_names.index(self.app.midi_out.name) 348 | else: 349 | self.app.midi_out_tmp_device_idx = -1 350 | self.app.midi_out_tmp_device_idx += increment 351 | if self.app.midi_out_tmp_device_idx >= len(self.app.available_midi_out_device_names): 352 | self.app.midi_out_tmp_device_idx = len(self.app.available_midi_out_device_names) - 1 353 | elif self.app.midi_out_tmp_device_idx < -1: 354 | self.app.midi_out_tmp_device_idx = -1 # Will use -1 for "None" 355 | 356 | elif encoder_name == push2_python.constants.ENCODER_TRACK4_ENCODER: 357 | self.app.set_midi_out_channel(self.app.midi_out_channel + increment, wrap=False) 358 | 359 | elif encoder_name == push2_python.constants.ENCODER_TRACK5_ENCODER: 360 | self.app.track_selection_mode.set_pyramidi_channel(self.app.track_selection_mode.pyramidi_channel + increment, wrap=False) 361 | 362 | elif encoder_name == push2_python.constants.ENCODER_TRACK6_ENCODER: 363 | if self.app.notes_midi_in_tmp_device_idx is None: 364 | if self.app.notes_midi_in is not None: 365 | self.app.notes_midi_in_tmp_device_idx = self.app.available_midi_in_device_names.index(self.app.notes_midi_in.name) 366 | else: 367 | self.app.notes_midi_in_tmp_device_idx = -1 368 | self.app.notes_midi_in_tmp_device_idx += increment 369 | if self.app.notes_midi_in_tmp_device_idx >= len(self.app.available_midi_in_device_names): 370 | self.app.notes_midi_in_tmp_device_idx = len(self.app.available_midi_in_device_names) - 1 371 | elif self.app.notes_midi_in_tmp_device_idx < -1: 372 | self.app.notes_midi_in_tmp_device_idx = -1 # Will use -1 for "None" 373 | 374 | elif self.current_page == 2: # About 375 | pass 376 | 377 | return True # Always return True because encoder should not be used in any other mode if this is first active 378 | 379 | def on_button_pressed(self, button_name): 380 | 381 | if self.current_page == 0: # Performance settings 382 | if button_name == push2_python.constants.BUTTON_UPPER_ROW_1: 383 | self.app.melodic_mode.set_root_midi_note(self.app.melodic_mode.root_midi_note + 1) 384 | self.app.pads_need_update = True 385 | return True 386 | 387 | elif button_name == push2_python.constants.BUTTON_UPPER_ROW_2: 388 | self.app.melodic_mode.use_poly_at = not self.app.melodic_mode.use_poly_at 389 | if self.app.melodic_mode.use_poly_at: 390 | self.app.push.pads.set_polyphonic_aftertouch() 391 | else: 392 | self.app.push.pads.set_channel_aftertouch() 393 | self.app.melodic_mode.set_lumi_pressure_mode() 394 | return True 395 | 396 | elif self.current_page == 1: # MIDI settings 397 | if button_name == push2_python.constants.BUTTON_UPPER_ROW_1: 398 | if self.app.midi_in_tmp_device_idx is None: 399 | if self.app.midi_in is not None: 400 | self.app.midi_in_tmp_device_idx = self.app.available_midi_in_device_names.index(self.app.midi_in.name) 401 | else: 402 | self.app.midi_in_tmp_device_idx = -1 403 | self.app.midi_in_tmp_device_idx += 1 404 | # Make index position wrap 405 | if self.app.midi_in_tmp_device_idx >= len(self.app.available_midi_in_device_names): 406 | self.app.midi_in_tmp_device_idx = -1 # Will use -1 for "None" 407 | elif self.app.midi_in_tmp_device_idx < -1: 408 | self.app.midi_in_tmp_device_idx = len(self.app.available_midi_in_device_names) - 1 409 | return True 410 | 411 | elif button_name == push2_python.constants.BUTTON_UPPER_ROW_2: 412 | self.app.set_midi_in_channel(self.app.midi_in_channel + 1, wrap=True) 413 | return True 414 | 415 | elif button_name == push2_python.constants.BUTTON_UPPER_ROW_3: 416 | if self.app.midi_out_tmp_device_idx is None: 417 | if self.app.midi_out is not None: 418 | self.app.midi_out_tmp_device_idx = self.app.available_midi_out_device_names.index(self.app.midi_out.name) 419 | else: 420 | self.app.midi_out_tmp_device_idx = -1 421 | self.app.midi_out_tmp_device_idx += 1 422 | # Make index position wrap 423 | if self.app.midi_out_tmp_device_idx >= len(self.app.available_midi_out_device_names): 424 | self.app.midi_out_tmp_device_idx = -1 # Will use -1 for "None" 425 | elif self.app.midi_out_tmp_device_idx < -1: 426 | self.app.midi_out_tmp_device_idx = len(self.app.available_midi_out_device_names) - 1 427 | return True 428 | 429 | elif button_name == push2_python.constants.BUTTON_UPPER_ROW_4: 430 | self.app.set_midi_out_channel(self.app.midi_out_channel + 1, wrap=True) 431 | return True 432 | 433 | elif button_name == push2_python.constants.BUTTON_UPPER_ROW_5: 434 | self.app.track_selection_mode.set_pyramidi_channel(self.app.track_selection_mode.pyramidi_channel + 1, wrap=False) 435 | return True 436 | 437 | elif button_name == push2_python.constants.BUTTON_UPPER_ROW_6: 438 | if self.app.notes_midi_in_tmp_device_idx is None: 439 | if self.app.notes_midi_in is not None: 440 | self.app.notes_midi_in_tmp_device_idx = self.app.available_midi_in_device_names.index(self.app.notes_midi_in.name) 441 | else: 442 | self.app.notes_midi_in_tmp_device_idx = -1 443 | self.app.notes_midi_in_tmp_device_idx += 1 444 | # Make index position wrap 445 | if self.app.notes_midi_in_tmp_device_idx >= len(self.app.available_midi_in_device_names): 446 | self.app.notes_midi_in_tmp_device_idx = -1 # Will use -1 for "None" 447 | elif self.app.notes_midi_in_tmp_device_idx < -1: 448 | self.app.notes_midi_in_tmp_device_idx = len(self.app.available_midi_in_device_names) - 1 449 | return True 450 | 451 | elif button_name == push2_python.constants.BUTTON_UPPER_ROW_7: 452 | self.app.on_midi_push_connection_established() 453 | return True 454 | 455 | elif self.current_page == 2: # About 456 | if button_name == push2_python.constants.BUTTON_UPPER_ROW_1: 457 | # Save current settings 458 | self.app.save_current_settings_to_file() 459 | return True 460 | 461 | elif button_name == push2_python.constants.BUTTON_UPPER_ROW_3: 462 | # Run software update code 463 | self.is_running_sw_update = True 464 | run_sw_update() 465 | return True 466 | 467 | 468 | def restart_program(): 469 | """Restarts the current program, with file objects and descriptors cleanup 470 | Source: https://stackoverflow.com/questions/11329917/restart-python-script-from-within-itself 471 | """ 472 | try: 473 | p = psutil.Process(os.getpid()) 474 | for handler in p.get_open_files() + p.connections(): 475 | os.close(handler.fd) 476 | except Exception as e: 477 | print(e) 478 | python = sys.executable 479 | os.execl(python, python, *sys.argv) 480 | 481 | 482 | def run_sw_update(): 483 | """Runs "git pull" in the current directory to retrieve latest code, then restart process""" 484 | print('Running SW update...') 485 | print('- installing dependencies') 486 | os.system('pip3 install -r requirements.txt --no-cache') 487 | print('- pulling from repository') 488 | os.system('git pull') 489 | print('- restarting process') 490 | restart_program() 491 | -------------------------------------------------------------------------------- /slice_notes_mode.py: -------------------------------------------------------------------------------- 1 | import definitions 2 | import push2_python.constants 3 | 4 | from melodic_mode import MelodicMode 5 | 6 | 7 | class SliceNotesMode(MelodicMode): 8 | 9 | start_note = 0 10 | color_groups = [ 11 | definitions.GREEN, 12 | definitions.YELLOW, 13 | definitions.ORANGE, 14 | definitions.RED, 15 | definitions.PINK, 16 | definitions.PURPLE, 17 | definitions.CYAN, 18 | definitions.BLUE 19 | ] 20 | 21 | def get_settings_to_save(self): 22 | return {} 23 | 24 | def pad_ij_to_midi_note(self, pad_ij): 25 | return self.start_note + 8 * (7 - pad_ij[0]) + pad_ij[1] 26 | 27 | def update_pads(self): 28 | color_matrix = [] 29 | for i in range(0, 8): 30 | row_colors = [] 31 | for j in range(0, 8): 32 | corresponding_midi_note = self.pad_ij_to_midi_note([i, j]) 33 | midi_16_note_groups_idx = corresponding_midi_note // 16 34 | #cell_color = self.color_groups[midi_16_note_groups_idx] 35 | if midi_16_note_groups_idx % 2 == 0: 36 | cell_color = self.app.track_selection_mode.get_current_track_color() 37 | else: 38 | cell_color = definitions.WHITE 39 | if self.is_midi_note_being_played(corresponding_midi_note): 40 | cell_color = definitions.NOTE_ON_COLOR 41 | row_colors.append(cell_color) 42 | color_matrix.append(row_colors) 43 | 44 | self.push.pads.set_pads_color(color_matrix) 45 | 46 | def on_button_pressed(self, button_name): 47 | 48 | if button_name == push2_python.constants.BUTTON_OCTAVE_UP: 49 | self.start_note += 16 50 | if self.start_note > 128 - 16 * 4: 51 | self.start_note = 128 - 16 * 4 52 | self.app.pads_need_update = True 53 | self.app.add_display_notification("MIDI notes range: {0} to {1}".format( 54 | self.pad_ij_to_midi_note((7, 0)), 55 | self.pad_ij_to_midi_note((0, 7)), 56 | )) 57 | return True 58 | 59 | elif button_name == push2_python.constants.BUTTON_OCTAVE_DOWN: 60 | self.start_note -= 16 61 | if self.start_note < 0: 62 | self.start_note = 0 63 | self.app.pads_need_update = True 64 | self.app.add_display_notification("MIDI notes range: {0} to {1}".format( 65 | self.pad_ij_to_midi_note((7, 0)), 66 | self.pad_ij_to_midi_note((0, 7)), 67 | )) 68 | return True 69 | 70 | else: 71 | # For the other buttons, refer to the base class 72 | super().on_button_pressed(button_name) 73 | -------------------------------------------------------------------------------- /track_listing.json: -------------------------------------------------------------------------------- 1 | [ 2 | "DDRM", 3 | "MINITAUR", 4 | "DOMINION", 5 | "KIJIMI", 6 | "OCTATRACK", 7 | "SOURCE", 8 | "-", 9 | "-", 10 | "DDRM", 11 | "MINITAUR", 12 | "DOMINION", 13 | "KIJIMI", 14 | "OCTATRACK", 15 | "SOURCE", 16 | "-", 17 | "-", 18 | "DDRM", 19 | "MINITAUR", 20 | "DOMINION", 21 | "KIJIMI", 22 | "OCTATRACK", 23 | "SOURCE", 24 | "-", 25 | "-", 26 | "DDRM", 27 | "MINITAUR", 28 | "DOMINION", 29 | "KIJIMI", 30 | "OCTATRACK", 31 | "SOURCE", 32 | "-", 33 | "-", 34 | "DDRM", 35 | "MINITAUR", 36 | "DOMINION", 37 | "KIJIMI", 38 | "OCTATRACK", 39 | "SOURCE", 40 | "-", 41 | "-", 42 | "DDRM", 43 | "MINITAUR", 44 | "DOMINION", 45 | "KIJIMI", 46 | "OCTATRACK", 47 | "SOURCE", 48 | "-", 49 | "-", 50 | "DDRM", 51 | "MINITAUR", 52 | "DOMINION", 53 | "KIJIMI", 54 | "OCTATRACK", 55 | "SOURCE", 56 | "-", 57 | "-", 58 | "DDRM", 59 | "MINITAUR", 60 | "DOMINION", 61 | "KIJIMI", 62 | "OCTATRACK", 63 | "SOURCE", 64 | "-", 65 | "-" 66 | ] -------------------------------------------------------------------------------- /track_selection_mode.py: -------------------------------------------------------------------------------- 1 | import definitions 2 | import mido 3 | import push2_python 4 | import time 5 | import math 6 | import os 7 | import json 8 | 9 | from display_utils import show_text 10 | 11 | 12 | class TrackSelectionMode(definitions.PyshaMode): 13 | 14 | tracks_info = [] 15 | track_button_names_a = [ 16 | push2_python.constants.BUTTON_LOWER_ROW_1, 17 | push2_python.constants.BUTTON_LOWER_ROW_2, 18 | push2_python.constants.BUTTON_LOWER_ROW_3, 19 | push2_python.constants.BUTTON_LOWER_ROW_4, 20 | push2_python.constants.BUTTON_LOWER_ROW_5, 21 | push2_python.constants.BUTTON_LOWER_ROW_6, 22 | push2_python.constants.BUTTON_LOWER_ROW_7, 23 | push2_python.constants.BUTTON_LOWER_ROW_8 24 | ] 25 | track_button_names_b = [ 26 | push2_python.constants.BUTTON_1_32T, 27 | push2_python.constants.BUTTON_1_32, 28 | push2_python.constants.BUTTON_1_16T, 29 | push2_python.constants.BUTTON_1_16, 30 | push2_python.constants.BUTTON_1_8T, 31 | push2_python.constants.BUTTON_1_8, 32 | push2_python.constants.BUTTON_1_4T, 33 | push2_python.constants.BUTTON_1_4 34 | ] 35 | track_selection_button_a = False 36 | track_selection_button_a_pressing_time = 0 37 | selected_track = 0 38 | track_selection_quick_press_time = 0.400 39 | pyramidi_channel = 15 40 | 41 | def initialize(self, settings=None): 42 | if settings is not None: 43 | self.pyramidi_channel = settings.get('pyramidi_channel', self.pyramidi_channel) 44 | 45 | self.create_tracks() 46 | 47 | def create_tracks(self): 48 | """This method creates 64 tracks corresponding to the Pyramid tracks that I use in my live setup. 49 | Instrument names are assigned according to the way I have them configured in the 64 tracks of Pyramid. 50 | Instrument names per track are loaded from "track_listing.json" file, and should correspond to instrument 51 | definition filenames from "instrument_definitions" folder. 52 | """ 53 | tmp_instruments_data = {} 54 | 55 | if os.path.exists(definitions.TRACK_LISTING_PATH): 56 | track_instruments = json.load(open(definitions.TRACK_LISTING_PATH)) 57 | for i, instrument_short_name in enumerate(track_instruments): 58 | if instrument_short_name not in tmp_instruments_data: 59 | try: 60 | instrument_data = json.load(open(os.path.join(definitions.INSTRUMENT_DEFINITION_FOLDER, '{}.json'.format(instrument_short_name)))) 61 | tmp_instruments_data[instrument_short_name] = instrument_data 62 | except FileNotFoundError: 63 | # No definition file for instrument exists 64 | instrument_data = {} 65 | else: 66 | instrument_data = tmp_instruments_data[instrument_short_name] 67 | color = instrument_data.get('color', None) 68 | if color is None: 69 | if instrument_short_name != '-': 70 | color = definitions.COLORS_NAMES[i % 8] 71 | else: 72 | color = definitions.GRAY_DARK 73 | self.tracks_info.append({ 74 | 'track_name': '{0}{1}'.format((i % 16) + 1, ['A', 'B', 'C', 'D'][i//16]), 75 | 'instrument_name': instrument_data.get('instrument_name', '-'), 76 | 'instrument_short_name': instrument_short_name, 77 | 'midi_channel': instrument_data.get('midi_channel', -1), 78 | 'color': color, 79 | 'n_banks': instrument_data.get('n_banks', 1), 80 | 'bank_names': instrument_data.get('bank_names', None), 81 | 'default_layout': instrument_data.get('default_layout', definitions.LAYOUT_MELODIC), 82 | 'illuminate_local_notes': instrument_data.get('illuminate_local_notes', True), 83 | }) 84 | print('Created {0} tracks!'.format(len(self.tracks_info))) 85 | else: 86 | # Create 64 empty tracks 87 | for i in range(0, 64): 88 | self.tracks_info.append({ 89 | 'track_name': '{0}{1}'.format((i % 16) + 1, ['A', 'B', 'C', 'D'][i//16]), 90 | 'instrument_name': '-', 91 | 'instrument_short_name': '-', 92 | 'midi_channel': -1, 93 | 'color': definitions.ORANGE, 94 | 'default_layout': definitions.LAYOUT_MELODIC, 95 | 'illuminate_local_notes': True, 96 | }) 97 | 98 | def get_settings_to_save(self): 99 | return { 100 | 'pyramidi_channel': self.pyramidi_channel, 101 | } 102 | 103 | def set_pyramidi_channel(self, channel, wrap=False): 104 | self.pyramidi_channel = channel 105 | if self.pyramidi_channel < 0: 106 | self.pyramidi_channel = 0 if not wrap else 15 107 | elif self.pyramidi_channel > 15: 108 | self.pyramidi_channel = 15 if not wrap else 0 109 | 110 | def get_all_distinct_instrument_short_names(self): 111 | return list(set([track['instrument_short_name'] for track in self.tracks_info])) 112 | 113 | def get_current_track_info(self): 114 | return self.tracks_info[self.selected_track] 115 | 116 | def get_current_track_instrument_short_name(self): 117 | return self.get_current_track_info()['instrument_short_name'] 118 | 119 | def get_track_color(self, i): 120 | return self.tracks_info[i]['color'] 121 | 122 | def get_current_track_color(self): 123 | return self.get_track_color(self.selected_track) 124 | 125 | def get_current_track_color_rgb(self): 126 | return definitions.get_color_rgb_float(self.get_current_track_color()) 127 | 128 | def load_current_default_layout(self): 129 | if self.get_current_track_info()['default_layout'] == definitions.LAYOUT_MELODIC: 130 | self.app.set_melodic_mode() 131 | elif self.get_current_track_info()['default_layout'] == definitions.LAYOUT_RHYTHMIC: 132 | self.app.set_rhythmic_mode() 133 | elif self.get_current_track_info()['default_layout'] == definitions.LAYOUT_SLICES: 134 | self.app.set_slice_notes_mode() 135 | 136 | def clean_currently_notes_being_played(self): 137 | if self.app.is_mode_active(self.app.melodic_mode): 138 | self.app.melodic_mode.remove_all_notes_being_played() 139 | elif self.app.is_mode_active(self.app.rhyhtmic_mode): 140 | self.app.rhyhtmic_mode.remove_all_notes_being_played() 141 | 142 | def send_select_track_to_pyramid(self, track_idx): 143 | # Follows pyramidi specification (Pyramid configured to receive on ch 16) 144 | msg = mido.Message('control_change', control=0, value=track_idx + 1, channel=self.pyramidi_channel) 145 | self.app.send_midi_to_pyramid(msg) 146 | 147 | def select_track(self, track_idx): 148 | # Selects a track and activates its melodic/rhythmic layout 149 | # Note that if this is called from a mode form the same xor group with melodic/rhythmic modes, 150 | # that other mode will be deactivated. 151 | self.selected_track = track_idx 152 | self.send_select_track_to_pyramid(self.selected_track) 153 | self.load_current_default_layout() 154 | self.clean_currently_notes_being_played() 155 | try: 156 | self.app.midi_cc_mode.new_track_selected() 157 | self.app.preset_selection_mode.new_track_selected() 158 | self.app.pyramid_track_triggering_mode.new_track_selected() 159 | except AttributeError: 160 | # Might fail if MIDICCMode/PresetSelectionMode/PyramidTrackTriggeringMode not initialized 161 | pass 162 | 163 | def activate(self): 164 | self.update_buttons() 165 | self.update_pads() 166 | 167 | def deactivate(self): 168 | for button_name in self.track_button_names_a + self.track_button_names_b: 169 | self.push.buttons.set_button_color(button_name, definitions.BLACK) 170 | 171 | def update_buttons(self): 172 | for count, name in enumerate(self.track_button_names_a): 173 | color = self.tracks_info[count]['color'] 174 | self.push.buttons.set_button_color(name, color) 175 | 176 | for count, name in enumerate(self.track_button_names_b): 177 | if self.track_selection_button_a: 178 | color = self.tracks_info[self.track_button_names_a.index(self.track_selection_button_a)]['color'] 179 | equivalent_track_num = self.track_button_names_a.index(self.track_selection_button_a) + count * 8 180 | if self.selected_track == equivalent_track_num: 181 | self.push.buttons.set_button_color(name, definitions.WHITE) 182 | self.push.buttons.set_button_color(name, color, animation=definitions.DEFAULT_ANIMATION) 183 | else: 184 | self.push.buttons.set_button_color(name, color) 185 | else: 186 | color = self.get_current_track_color() 187 | equivalent_track_num = (self.selected_track % 8) + count * 8 188 | if self.selected_track == equivalent_track_num: 189 | self.push.buttons.set_button_color(name, definitions.WHITE) 190 | self.push.buttons.set_button_color(name, color, animation=definitions.DEFAULT_ANIMATION) 191 | else: 192 | self.push.buttons.set_button_color(name, color) 193 | 194 | def update_display(self, ctx, w, h): 195 | 196 | # Draw track selector labels 197 | height = 20 198 | for i in range(0, 8): 199 | track_color = self.tracks_info[i]['color'] 200 | if self.selected_track % 8 == i: 201 | background_color = track_color 202 | font_color = definitions.BLACK 203 | else: 204 | background_color = definitions.BLACK 205 | font_color = track_color 206 | instrument_short_name = self.tracks_info[i]['instrument_short_name'] 207 | show_text(ctx, i, h - height, instrument_short_name, height=height, 208 | font_color=font_color, background_color=background_color) 209 | 210 | def on_button_pressed(self, button_name): 211 | if button_name in self.track_button_names_a: 212 | self.track_selection_button_a = button_name 213 | self.track_selection_button_a_pressing_time = time.time() 214 | self.app.buttons_need_update = True 215 | return True 216 | 217 | elif button_name in self.track_button_names_b: 218 | if self.track_selection_button_a: 219 | # While pressing one of the track selection a buttons 220 | self.select_track(self.track_button_names_a.index( 221 | self.track_selection_button_a) + self.track_button_names_b.index(button_name) * 8) 222 | self.app.buttons_need_update = True 223 | self.app.pads_need_update = True 224 | self.track_selection_button_a = False 225 | self.track_selection_button_a_pressing_time = 0 226 | return True 227 | else: 228 | # No track selection a button being pressed... 229 | self.select_track(self.selected_track % 8 + 8 * self.track_button_names_b.index(button_name)) 230 | self.app.buttons_need_update = True 231 | self.app.pads_need_update = True 232 | return True 233 | 234 | def on_button_released(self, button_name): 235 | if button_name in self.track_button_names_a: 236 | if self.track_selection_button_a: 237 | if time.time() - self.track_selection_button_a_pressing_time < self.track_selection_quick_press_time: 238 | # Only switch to track if it was a quick press 239 | self.select_track(self.track_button_names_a.index(button_name)) 240 | self.track_selection_button_a = False 241 | self.track_selection_button_a_pressing_time = 0 242 | self.app.buttons_need_update = True 243 | self.app.pads_need_update = True 244 | return True 245 | --------------------------------------------------------------------------------