├── .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 |
--------------------------------------------------------------------------------