├── .gitattributes ├── .gitignore ├── .idea ├── .gitignore ├── Micropython-midi-library.iml ├── inspectionProfiles │ └── profiles_settings.xml ├── libraries │ └── MicroPython.xml ├── misc.xml └── modules.xml ├── Fritzing schematics ├── midi_in.fzz ├── midi_out.fzz └── midilib.fzz ├── LICENSE ├── README.md ├── examples ├── main_midi_to_cv.py ├── main_rx_test.py ├── main_sysex_tx.py ├── main_thru_test.py └── main_tx_test.py ├── main.py └── midi.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .nox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | .pytest_cache/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | db.sqlite3 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # IPython 77 | profile_default/ 78 | ipython_config.py 79 | 80 | # pyenv 81 | .python-version 82 | 83 | # celery beat schedule file 84 | celerybeat-schedule 85 | 86 | # SageMath parsed files 87 | *.sage.py 88 | 89 | # Environments 90 | .env 91 | .venv 92 | env/ 93 | venv/ 94 | ENV/ 95 | env.bak/ 96 | venv.bak/ 97 | 98 | # Spyder project settings 99 | .spyderproject 100 | .spyproject 101 | 102 | # Rope project settings 103 | .ropeproject 104 | 105 | # mkdocs documentation 106 | /site 107 | 108 | # mypy 109 | .mypy_cache/ 110 | .dmypy.json 111 | dmypy.json 112 | 113 | # Pyre type checker 114 | .pyre/ 115 | .idea/vcs.xml 116 | main.py 117 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/Micropython-midi-library.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/libraries/MicroPython.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Fritzing schematics/midi_in.fzz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sensai7/Micropython-midi-library/cb36e8e2f0e2623a4f83edf98881f01489ad90f6/Fritzing schematics/midi_in.fzz -------------------------------------------------------------------------------- /Fritzing schematics/midi_out.fzz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sensai7/Micropython-midi-library/cb36e8e2f0e2623a4f83edf98881f01489ad90f6/Fritzing schematics/midi_out.fzz -------------------------------------------------------------------------------- /Fritzing schematics/midilib.fzz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sensai7/Micropython-midi-library/cb36e8e2f0e2623a4f83edf98881f01489ad90f6/Fritzing schematics/midilib.fzz -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 sensai7 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 | # Micropython-midi-library 2 | A complete MIDI I/O library for Micropython devices 3 | 4 | To use, simply download the midi.py file and add it to your project. Availability thorugh pip is on the works. 5 | 6 | At the current state this library supports all types of MIDI messages via input or output. 7 | 8 | Some usage examples are provided in the /examples folder. 9 | 10 | ## Quick Reference 11 | 12 | ### Initializing a MIDI instance object 13 | 14 | ```python 15 | MIDI_TX = Pin(8) # Optocoupled MIDI input in general purpose pin 8 (UART1TX) 16 | MIDI_RX = Pin(9) # MIDI output in general purpose pin 9 (UART1RX) 17 | UART_1 = 1 18 | my_midi = midi.Midi(UART_1, tx=MIDI_TX, rx=MIDI_RX) 19 | ``` 20 | 21 | ### Sending MIDI messages 22 | 23 | ![midi_out_circuit](https://i.imgur.com/Sw4neOf.png) 24 | 25 | ```python 26 | my_midi.send_note_on(midi.CHANNEL[1], midi.NOTE_CODE["C#4"], velocity=100) 27 | my_midi.send_note_off(midi.CHANNEL[1], midi.NOTE_CODE["C#4"]) 28 | my_midi.send_control_change(midi.CHANNEL[1], midi.CONTROL_CHANGE_CODE["MODULATION_WHEEL"], value=80) 29 | # (...Other send methods) 30 | ``` 31 | 32 | ### Receiving MIDI messages 33 | 34 | ![midi_in_circuit](https://i.imgur.com/4kmR5en.png) 35 | 36 | ```python 37 | while True: 38 | if my_midi.any() > 0: 39 | my_midi.load_message(my_midi.read(1)) 40 | if my_midi.last_sequence == midi.MIDI_SEQUENCE["NOTE_ON"]: 41 | channel = my_midi.channel 42 | note = my_midi.get_parameter("note_on", "note") 43 | velocity = my_midi.get_parameter("note_on", "velocity") 44 | # do something with these parameters 45 | 46 | if my_midi.last_sequence == midi.MIDI_SEQUENCE["NOTE_OFF"]: 47 | channel = my_midi.channel 48 | note = my_midi.get_parameter("note_off", "note") 49 | # do something with this parameter 50 | 51 | # (...Other receive blocks) 52 | ``` 53 | -------------------------------------------------------------------------------- /examples/main_midi_to_cv.py: -------------------------------------------------------------------------------- 1 | # generic MIDI to CV/GATE 2 | from machine import Pin, PWM 3 | import midi 4 | 5 | MIDI_TX = Pin(8) # Optocoupled MIDI input in general purpose pin 8 (UART1TX) 6 | MIDI_RX = Pin(9) # MIDI output in general purpose pin 9 (UART1RX) 7 | GATE = Pin(1) # Gate out in general purpose pin 8 8 | PWM_NOTE = PWM(Pin(20)) # Note CV PWM out in general purpose pin 20 9 | PWM_VELOCITY = PWM(Pin(21)) # Velocity CV PWM out in general purpose pin 21 10 | 11 | my_midi = midi.Midi(1, tx=MIDI_TX, rx=MIDI_RX) 12 | note_balance = 0 13 | PWM_NOTE.freq(10000) 14 | PWM_VELOCITY.freq(10000) 15 | 16 | while True: 17 | if my_midi.any() > 0: 18 | byte = my_midi.read(1) 19 | my_midi.load_message(byte) 20 | if my_midi.last_sequence == midi.MIDI_SEQUENCE["NOTE_ON"]: 21 | note = my_midi.last_rx_parameters["note_on"]["note"] 22 | velocity = my_midi.last_rx_parameters["note_on"]["velocity"] 23 | note_duty = (note - 36) * pow(2, 16) // 60 # Pitch CV 0V~3.3V (C2~C7) 24 | velocity_duty = (velocity + 1) * pow(2, 9) - 1 # Velocity CV 0V~3.3V 25 | PWM_NOTE.duty_u16(note_duty) 26 | PWM_VELOCITY.duty_u16(velocity_duty) 27 | note_balance += 1 28 | GATE.high() 29 | 30 | if my_midi.last_sequence == midi.MIDI_SEQUENCE["NOTE_OFF"]: 31 | note_balance -= 1 32 | if note_balance == 0: 33 | GATE.low() 34 | -------------------------------------------------------------------------------- /examples/main_rx_test.py: -------------------------------------------------------------------------------- 1 | # generic midi send program 2 | from machine import Pin 3 | import midi 4 | 5 | MIDI_TX = Pin(8) # Optocoupled MIDI input in general purpose pin 8 (UART1TX) 6 | MIDI_RX = Pin(9) # MIDI output in general purpose pin 9 (UART1RX) 7 | led = Pin(25, Pin.OUT) 8 | 9 | my_midi = midi.Midi(1, tx=MIDI_TX, rx=MIDI_RX) 10 | 11 | 12 | while True: 13 | if my_midi.any() > 0: 14 | byte = my_midi.read(1) 15 | my_midi.load_message(byte) 16 | 17 | if my_midi.last_sequence == midi.MIDI_SEQUENCE["NOTE_ON"]: 18 | channel = my_midi.channel 19 | note = my_midi.get_parameter("note_on", "note") 20 | velocity = my_midi.get_parameter("note_on", "velocity") 21 | # do something with these parameters 22 | led.on() 23 | 24 | if my_midi.last_sequence == midi.MIDI_SEQUENCE["NOTE_OFF"]: 25 | channel = my_midi.channel 26 | note = my_midi.get_parameter("note_off", "note") 27 | # do something with this parameter 28 | led.off() 29 | -------------------------------------------------------------------------------- /examples/main_sysex_tx.py: -------------------------------------------------------------------------------- 1 | # Send a SYSEX sequence 2 | from machine import Pin 3 | import midi 4 | 5 | MIDI_TX = Pin(8) # Optocoupled MIDI input in general purpose pin 8 (UART1TX) 6 | MIDI_RX = Pin(9) # MIDI output in general purpose pin 9 (UART1RX) 7 | 8 | my_midi = midi.Midi(1, tx=MIDI_TX, rx=MIDI_RX) 9 | midi_sysex = [0x20, 0x19, 0x13, 0x20, 0x05, 0x29, 0x26, 0x08, 0x20, 0x22, 0x17, 0x30, 0x02, 0x21, 0x22, 10 | 0x13, 0x12, 0x25, 0x28, 0x26, 0x14, 0x29, 0x25, 0x10, 0x06, 0x28, 0x02, 0x23, 0x19, 0x12, 11 | 0x20, 0x04, 0x17, 0x23, 0x26, 0x15, 0x16, 0x12, 0x08, 0x08, 0x17, 0x11, 0x05, 0x01, 0x21, 12 | 0x19, 0x24, 0x07, 0x26, 0x29, 0x02, 0x28, 0x01, 0x23, 0x30, 0x07, 0x10, 0x30, 0x05, 0x03, 13 | 0x00, 0x26, 0x03, 0x30, 0x24, 0x25, 0x12, 0x08, 0x01, 0x27, 0x26, 0x06, 0x29, 0x16, 0x17, 14 | 0x12, 0x27, 0x26, 0x12, 0x22, 0x23, 0x00, 0x13, 0x11, 0x25, 0x26, 0x28, 0x19, 0x2, 15 | 0x13, 0x10, 0x08, 0x20, 0x16, 0x28, 0x05, 0x30, 0x17, 0x02, 0x00] 16 | 17 | my_midi.send_sysex_start() # sends 0xF0 to start a system exclusive sequence 18 | my_midi.send_sysex(midi_sysex) # loops through the sysex commands list 19 | my_midi.send_sysex_stop() # sends 0xF7 to stop a system exclusive sequence 20 | -------------------------------------------------------------------------------- /examples/main_thru_test.py: -------------------------------------------------------------------------------- 1 | # generic midi thru program 2 | from machine import Pin 3 | import midi 4 | 5 | MIDI_TX = Pin(8) # Optocoupled MIDI input in general purpose pin 8 (UART1TX) 6 | MIDI_RX = Pin(9) # MIDI output in general purpose pin 9 (UART1RX) 7 | led = Pin(25, Pin.OUT) 8 | 9 | my_midi = midi.Midi(1, tx=MIDI_TX, rx=MIDI_RX) 10 | 11 | while True: 12 | if my_midi.any() > 0: 13 | byte = my_midi.read(1) 14 | my_midi.load_message(byte) 15 | my_midi.write(my_midi.message) 16 | -------------------------------------------------------------------------------- /examples/main_tx_test.py: -------------------------------------------------------------------------------- 1 | # generic midi send program 2 | from machine import Pin 3 | import midi 4 | import time 5 | 6 | MIDI_TX = Pin(8) # Optocoupled MIDI input in general purpose pin 8 (UART1TX) 7 | MIDI_RX = Pin(9) # MIDI output in general purpose pin 9 (UART1RX) 8 | led = Pin(25, Pin.OUT) 9 | 10 | my_midi = midi.Midi(1, tx=MIDI_TX, rx=MIDI_RX) 11 | 12 | led.on() 13 | my_midi.send_note_on(midi.CHANNEL[1], midi.NOTE_CODE["C#4"], velocity=100) 14 | led.off() 15 | time.sleep(1) 16 | 17 | led.on() 18 | my_midi.send_note_off(midi.CHANNEL[1], midi.NOTE_CODE["C#4"]) 19 | led.off() 20 | time.sleep(1) 21 | 22 | led.on() 23 | my_midi.send_control_change(midi.CHANNEL[1], midi.CONTROL_CHANGE_CODE["MODULATION_WHEEL"], value=80) 24 | led.off() 25 | time.sleep(1) 26 | 27 | led.on() 28 | my_midi.send_channel_aftertouch(midi.CHANNEL[1], amount=23) 29 | led.off() 30 | time.sleep(1) 31 | 32 | led.on() 33 | my_midi.send_playback_start() 34 | led.off() 35 | time.sleep(1) 36 | 37 | led.on() 38 | my_midi.send_playback_continue() 39 | led.off() 40 | time.sleep(1) 41 | 42 | led.on() 43 | my_midi.send_playback_stop() 44 | led.off() 45 | time.sleep(1) 46 | 47 | led.on() 48 | my_midi.send_sysex_start() 49 | led.off() 50 | time.sleep(1) 51 | 52 | led.on() 53 | my_midi.send_sysex([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) 54 | led.off() 55 | time.sleep(1) 56 | 57 | led.on() 58 | my_midi.send_sysex_stop() 59 | led.off() 60 | time.sleep(1) 61 | 62 | led.on() 63 | my_midi.send_poly_aftertouch(midi.CHANNEL[10], midi.NOTE_CODE['C6'], pressure=24) 64 | led.off() 65 | time.sleep(1) 66 | 67 | led.on() 68 | my_midi.send_program_change(midi.CHANNEL[3], program=21) 69 | led.off() 70 | time.sleep(1) 71 | 72 | led.on() 73 | my_midi.send_timing_clock() 74 | led.off() 75 | time.sleep(1) 76 | 77 | led.on() 78 | my_midi.send_tune_request() 79 | led.off() 80 | time.sleep(1) 81 | 82 | led.on() 83 | my_midi.send_song_select(10) 84 | led.off() 85 | time.sleep(1) 86 | 87 | led.on() 88 | my_midi.send_song_position_pointer(64) 89 | led.off() 90 | time.sleep(1) 91 | 92 | led.on() 93 | my_midi.send_time_code_qtr_frame(midi.MIDI_QTR_FRAME_CODE["30 fps"], hours=23, minutes=24, seconds=14, frames=1) 94 | led.off() 95 | time.sleep(1) 96 | 97 | led.on() 98 | my_midi.send_active_sensing() 99 | led.off() 100 | time.sleep(1) 101 | 102 | led.on() 103 | my_midi.send_reset() 104 | led.off() 105 | time.sleep(1) 106 | 107 | 108 | print("Execution finished.") 109 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # Send a SYSEX sequence 2 | from machine import Pin 3 | import midi 4 | 5 | MIDI_TX = Pin(8) # Optocoupled MIDI input in general purpose pin 8 (UART1TX) 6 | MIDI_RX = Pin(9) # MIDI output in general purpose pin 9 (UART1RX) 7 | 8 | my_midi = midi.Midi(1, tx=MIDI_TX, rx=MIDI_RX) 9 | midi_sysex = [0x20, 0x19, 0x13, 0x20, 0x05, 0x29, 0x26, 0x08, 0x20, 0x22, 0x17, 0x30, 0x02, 0x21, 0x22, 10 | 0x13, 0x12, 0x25, 0x28, 0x26, 0x14, 0x29, 0x25, 0x10, 0x06, 0x28, 0x02, 0x23, 0x19, 0x12, 11 | 0x20, 0x04, 0x17, 0x23, 0x26, 0x15, 0x16, 0x12, 0x08, 0x08, 0x17, 0x11, 0x05, 0x01, 0x21, 12 | 0x19, 0x24, 0x07, 0x26, 0x29, 0x02, 0x28, 0x01, 0x23, 0x30, 0x07, 0x10, 0x30, 0x05, 0x03, 13 | 0x00, 0x26, 0x03, 0x30, 0x24, 0x25, 0x12, 0x08, 0x01, 0x27, 0x26, 0x06, 0x29, 0x16, 0x17, 14 | 0x12, 0x27, 0x26, 0x12, 0x22, 0x23, 0x00, 0x13, 0x11, 0x25, 0x26, 0x28, 0x19, 0x2, 15 | 0x13, 0x10, 0x08, 0x20, 0x16, 0x28, 0x05, 0x30, 0x17, 0x02, 0x00] 16 | 17 | my_midi.send_sysex_start() # sends 0xF0 to start a system exclusive sequence 18 | my_midi.send_sysex(midi_sysex) # loops through the sysex commands list 19 | my_midi.send_sysex_stop() # sends 0xF7 to stop a system exclusive sequence 20 | -------------------------------------------------------------------------------- /midi.py: -------------------------------------------------------------------------------- 1 | from machine import UART 2 | 3 | # generic MIDI macros 4 | BAUD = 31250 5 | FREQUENCY = BAUD 6 | 7 | # MIDI message macros 8 | MESSAGE_NOTE_OFF = 0x80 9 | MESSAGE_NOTE_ON = 0x90 10 | MESSAGE_POLYPHONIC_AFTERTOUCH = 0XA0 11 | MESSAGE_CONTROL_CHANGE = 0xB0 12 | MESSAGE_PROGRAM_CHANGE = 0xC0 13 | MESSAGE_CHANNEL_AFTERTOUCH = 0xD0 14 | MESSAGE_PITCH_BEND = 0xE0 15 | MESSAGE_SYSTEM_EXCLUSIVE_START = 0xF0 16 | MESSAGE_MIDI_TIME_CODE_QTR_FRAME = 0xF1 17 | MESSAGE_SONG_POSITION_POINTER = 0xF2 18 | MESSAGE_SONG_SELECT = 0xF3 19 | # 0xF4 and 0xF5 reserved 20 | MESSAGE_TUNE_REQUEST = 0xF6 21 | MESSAGE_SYSTEM_EXCLUSIVE_STOP = 0xF7 22 | MESSAGE_TIMING_CLOCK = 0xF8 23 | # 0xF9 reserved 24 | MESSAGE_START = 0xFA 25 | MESSAGE_CONTINUE = 0xFB 26 | MESSAGE_STOP = 0xFC 27 | # 0xFD reserved 28 | MESSAGE_ACTIVE_SENSING = 0xFE 29 | MESSAGE_SYSTEM_RESET = 0xFF 30 | 31 | CHANNEL = { 32 | 1: 0, 33 | 2: 1, 34 | 3: 2, 35 | 4: 3, 36 | 5: 4, 37 | 6: 5, 38 | 7: 6, 39 | 8: 7, 40 | 9: 8, 41 | 10: 9, 42 | 11: 10, 43 | 12: 11, 44 | 13: 12, 45 | 14: 13, 46 | 15: 14, 47 | 16: 15, 48 | } 49 | 50 | CONTROL_CHANGE_CODE = { 51 | 0: "BANK_SELECT", 52 | 1: "MODULATION_WHEEL", 53 | 2: "BREATH_CONTROLLER", 54 | 3: "UNDEFINED", 55 | 4: "FOOT_CONTROLLER", 56 | 5: "PORTAMENTO_TIME", 57 | 6: "DATA_ENTRY", 58 | 7: "CHANNEL_VOLUME", 59 | 8: "BALANCE", 60 | 9: "UNDEFINED", 61 | 10: "PAN", 62 | 11: "EXPRESSION_CONTROLLER", 63 | 12: "EFFECT_CONTROL_1", 64 | 13: "EFFECT_CONTROL_2", 65 | 14: "UNDEFINED", 66 | 15: "UNDEFINED", 67 | 16: "GENERAL_PURPOSE_CONTROLLER_1", 68 | 17: "GENERAL_PURPOSE_CONTROLLER_2", 69 | 18: "GENERAL_PURPOSE_CONTROLLER_3", 70 | 19: "GENERAL_PURPOSE_CONTROLLER_4", 71 | 20: "UNDEFINED", 72 | 21: "UNDEFINED", 73 | 22: "UNDEFINED", 74 | 23: "UNDEFINED", 75 | 24: "UNDEFINED", 76 | 25: "UNDEFINED", 77 | 26: "UNDEFINED", 78 | 27: "UNDEFINED", 79 | 28: "UNDEFINED", 80 | 29: "UNDEFINED", 81 | 30: "UNDEFINED", 82 | 31: "UNDEFINED", 83 | 32: "CONTROL_0", 84 | 33: "CONTROL_1", 85 | 34: "CONTROL_2", 86 | 35: "CONTROL_3", 87 | 36: "CONTROL_4", 88 | 37: "CONTROL_5", 89 | 38: "CONTROL_6", 90 | 39: "CONTROL_7", 91 | 40: "CONTROL_8", 92 | 41: "CONTROL_9", 93 | 42: "CONTROL_10", 94 | 43: "CONTROL_11", 95 | 44: "CONTROL_12", 96 | 45: "CONTROL_13", 97 | 46: "CONTROL_14", 98 | 47: "CONTROL_15", 99 | 48: "CONTROL_16", 100 | 49: "CONTROL_17", 101 | 50: "CONTROL_18", 102 | 51: "CONTROL_19", 103 | 52: "CONTROL_20", 104 | 53: "CONTROL_21", 105 | 54: "CONTROL_22", 106 | 55: "CONTROL_23", 107 | 56: "CONTROL_24", 108 | 57: "CONTROL_25", 109 | 58: "CONTROL_26", 110 | 59: "CONTROL_27", 111 | 60: "CONTROL_28", 112 | 61: "CONTROL_29", 113 | 62: "CONTROL_30", 114 | 63: "CONTROL_31", 115 | 64: "DAMPER_PEDAL_ON_OFF", 116 | 65: "PORTAMENTO_ON_OFF", 117 | 66: "SOSTENUTO_ON_OFF", 118 | 67: "SOFT_PEDAL_ON_OFF", 119 | 68: "LEGATO_FOOTSWITCH", 120 | 69: "HOLD_2", 121 | 70: "SOUND_CONTROLLER_1", 122 | 71: "SOUND_CONTROLLER_2", 123 | 72: "SOUND_CONTROLLER_3", 124 | 73: "SOUND_CONTROLLER_4", 125 | 74: "SOUND_CONTROLLER_5", 126 | 75: "SOUND_CONTROLLER_6", 127 | 76: "SOUND_CONTROLLER_7", 128 | 77: "SOUND_CONTROLLER_8", 129 | 78: "SOUND_CONTROLLER_9", 130 | 79: "SOUND_CONTROLLER_10", 131 | 80: "GENERAL_PURPOSE_CONTROLLER_5", 132 | 81: "GENERAL_PURPOSE_CONTROLLER_6", 133 | 82: "GENERAL_PURPOSE_CONTROLLER_7", 134 | 83: "GENERAL_PURPOSE_CONTROLLER_8", 135 | 84: "PORTAMENTO_CONTROL", 136 | 85: "UNDEFINED", 137 | 86: "UNDEFINED", 138 | 87: "UNDEFINED", 139 | 88: "HIGH_RESOLUTION_VELOCITY_PREFIX", 140 | 89: "UNDEFINED", 141 | 90: "UNDEFINED", 142 | 91: "EFFECTS_1_DEPTH", 143 | 92: "EFFECTS_2_DEPTH", 144 | 93: "EFFECTS_3_DEPTH", 145 | 94: "EFFECTS_4_DEPTH", 146 | 95: "EFFECTS_5_DEPTH", 147 | 96: "DATA_INCREMENT", 148 | 97: "DATA_DECREMENT", 149 | 98: "NON_REGISTERED_PARAMETER_NUMBER_LSB", 150 | 99: "NON_REGISTERED_PARAMETER_NUMBER_MSB", 151 | 100: "REGISTERED_PARAMETER_NUMBER_LSB", 152 | 101: "REGISTERED_PARAMETER_NUMBER_MSB", 153 | 102: "UNDEFINED", 154 | 103: "UNDEFINED", 155 | 104: "UNDEFINED", 156 | 105: "UNDEFINED", 157 | 106: "UNDEFINED", 158 | 107: "UNDEFINED", 159 | 108: "UNDEFINED", 160 | 109: "UNDEFINED", 161 | 110: "UNDEFINED", 162 | 111: "UNDEFINED", 163 | 112: "UNDEFINED", 164 | 113: "UNDEFINED", 165 | 114: "UNDEFINED", 166 | 115: "UNDEFINED", 167 | 116: "UNDEFINED", 168 | 117: "UNDEFINED", 169 | 118: "UNDEFINED", 170 | 119: "UNDEFINED", 171 | 120: "ALL_SOUND_OFF", 172 | 121: "RESET_ALL_CONTROLLERS", 173 | 122: "LOCAL_CONTROL_ON_OFF", 174 | 123: "ALL_NOTES_OFF", 175 | 124: "OMNI_MODE_OFF", 176 | 125: "OMNI_MODE_ON", 177 | 126: "MONO_MODE_ON", 178 | 127: "POLY_MODE_ON", 179 | "BANK_SELECT": 0, 180 | "MODULATION_WHEEL": 1, 181 | "BREATH_CONTROLLER": 2, 182 | "FOOT_CONTROLLER": 4, 183 | "PORTAMENTO_TIME": 5, 184 | "DATA_ENTRY": 6, 185 | "CHANNEL_VOLUME": 7, 186 | "BALANCE": 8, 187 | "PAN": 10, 188 | "EXPRESSION_CONTROLLER": 11, 189 | "EFFECT_CONTROL_1": 12, 190 | "EFFECT_CONTROL_2": 13, 191 | "GENERAL_PURPOSE_CONTROLLER_1": 16, 192 | "GENERAL_PURPOSE_CONTROLLER_2": 17, 193 | "GENERAL_PURPOSE_CONTROLLER_3": 18, 194 | "GENERAL_PURPOSE_CONTROLLER_4": 19, 195 | "CONTROL_0": 32, 196 | "CONTROL_1": 33, 197 | "CONTROL_2": 34, 198 | "CONTROL_3": 35, 199 | "CONTROL_4": 36, 200 | "CONTROL_5": 37, 201 | "CONTROL_6": 38, 202 | "CONTROL_7": 39, 203 | "CONTROL_8": 40, 204 | "CONTROL_9": 41, 205 | "CONTROL_10": 42, 206 | "CONTROL_11": 43, 207 | "CONTROL_12": 44, 208 | "CONTROL_13": 45, 209 | "CONTROL_14": 46, 210 | "CONTROL_15": 47, 211 | "CONTROL_16": 48, 212 | "CONTROL_17": 49, 213 | "CONTROL_18": 50, 214 | "CONTROL_19": 51, 215 | "CONTROL_20": 52, 216 | "CONTROL_21": 53, 217 | "CONTROL_22": 54, 218 | "CONTROL_23": 55, 219 | "CONTROL_24": 56, 220 | "CONTROL_25": 57, 221 | "CONTROL_26": 58, 222 | "CONTROL_27": 59, 223 | "CONTROL_28": 60, 224 | "CONTROL_29": 61, 225 | "CONTROL_30": 62, 226 | "CONTROL_31": 63, 227 | "DAMPER_PEDAL_ON_OFF": 64, 228 | "PORTAMENTO_ON_OFF": 65, 229 | "SOSTENUTO_ON_OFF": 66, 230 | "SOFT_PEDAL_ON_OFF": 67, 231 | "LEGATO_FOOTSWITCH": 68, 232 | "HOLD_2": 69, 233 | "SOUND_CONTROLLER_1": 70, 234 | "SOUND_CONTROLLER_2": 71, 235 | "SOUND_CONTROLLER_3": 72, 236 | "SOUND_CONTROLLER_4": 73, 237 | "SOUND_CONTROLLER_5": 74, 238 | "SOUND_CONTROLLER_6": 75, 239 | "SOUND_CONTROLLER_7": 76, 240 | "SOUND_CONTROLLER_8": 77, 241 | "SOUND_CONTROLLER_9": 78, 242 | "SOUND_CONTROLLER_10": 79, 243 | "GENERAL_PURPOSE_CONTROLLER_5": 80, 244 | "GENERAL_PURPOSE_CONTROLLER_6": 81, 245 | "GENERAL_PURPOSE_CONTROLLER_7": 82, 246 | "GENERAL_PURPOSE_CONTROLLER_8": 83, 247 | "PORTAMENTO_CONTROL": 84, 248 | "HIGH_RESOLUTION_VELOCITY_PREFIX": 88, 249 | "EFFECTS_1_DEPTH": 91, 250 | "EFFECTS_2_DEPTH": 92, 251 | "EFFECTS_3_DEPTH": 93, 252 | "EFFECTS_4_DEPTH": 94, 253 | "EFFECTS_5_DEPTH": 95, 254 | "DATA_INCREMENT": 96, 255 | "DATA_DECREMENT": 97, 256 | "NON_REGISTERED_PARAMETER_NUMBER_LSB": 98, 257 | "NON_REGISTERED_PARAMETER_NUMBER_MSB": 99, 258 | "REGISTERED_PARAMETER_NUMBER_LSB": 100, 259 | "REGISTERED_PARAMETER_NUMBER_MSB": 101, 260 | "ALL_SOUND_OFF": 120, 261 | "RESET_ALL_CONTROLLERS": 121, 262 | "LOCAL_CONTROL_ON_OFF": 122, 263 | "ALL_NOTES_OFF": 123, 264 | "OMNI_MODE_OFF": 124, 265 | "OMNI_MODE_ON": 125, 266 | "MONO_MODE_ON": 126, 267 | "POLY_MODE_ON": 127, 268 | "UNDEFINED": [3, 9, 14, 15, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 85, 86, 87, 89, 90, 102, 269 | 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119] 270 | } 271 | 272 | NOTE_CODE = { 273 | 127: ["G9", 12543.85], 274 | 126: ["F#9", 11839.82], 275 | 125: ["F9", 11175.30], 276 | 124: ["E9", 10548.08], 277 | 123: ["D#9", 9956.06], 278 | 122: ["D9", 9397.27], 279 | 121: ["C#9", 8869.84], 280 | 120: ["C9", 8372.02], 281 | 119: ["B8", 7902.13], 282 | 118: ["A#8", 7458.62], 283 | 117: ["A8", 7040.00], 284 | 116: ["G#8", 6644.88], 285 | 115: ["G8", 6271.93], 286 | 114: ["F#8", 5919.91], 287 | 113: ["F8", 5587.65], 288 | 112: ["E8", 5274.04], 289 | 111: ["D#8", 4978.03], 290 | 110: ["D8", 4698.64], 291 | 109: ["C#8", 4434.92], 292 | 108: ["C8", 4186.01], 293 | 107: ["B7", 3951.07], 294 | 106: ["A#7", 3729.31], 295 | 105: ["A7", 3520.00], 296 | 104: ["G#7", 3322.44], 297 | 103: ["G7", 3135.96], 298 | 102: ["F#7", 2959.96], 299 | 101: ["F7", 2793.83], 300 | 100: ["E7", 2637.02], 301 | 99: ["D#7", 2489.02], 302 | 98: ["D7", 2349.32], 303 | 97: ["C#7", 2217.46], 304 | 96: ["C7", 2093.00], 305 | 95: ["B6", 1975.53], 306 | 94: ["A#6", 1864.66], 307 | 93: ["A6", 1760.00], 308 | 92: ["G#6", 1661.22], 309 | 91: ["G6", 1567.98], 310 | 90: ["F#6", 1479.98], 311 | 89: ["F6", 1396.91], 312 | 88: ["E6", 1318.51], 313 | 87: ["D#6", 1244.51], 314 | 86: ["D6", 1174.66], 315 | 85: ["C#6", 1108.73], 316 | 84: ["C6", 1046.50], 317 | 83: ["B5", 987.77], 318 | 82: ["A#5", 932.33], 319 | 81: ["A5", 880.00], 320 | 80: ["G#5", 830.61], 321 | 79: ["G5", 783.99], 322 | 78: ["F#5", 739.99], 323 | 77: ["F5", 698.46], 324 | 76: ["E5", 659.26], 325 | 75: ["D#5", 622.25], 326 | 74: ["D5", 587.33], 327 | 73: ["C#5", 554.37], 328 | 72: ["C5", 523.25], 329 | 71: ["B4", 493.88], 330 | 70: ["A#4", 466.16], 331 | 69: ["A4", 440.00], 332 | 68: ["G#4", 415.30], 333 | 67: ["G4", 392.00], 334 | 66: ["F#4", 369.99], 335 | 65: ["F4", 349.23], 336 | 64: ["E4", 329.63], 337 | 63: ["D#4", 311.13], 338 | 62: ["D4", 293.66], 339 | 61: ["C#4", 277.18], 340 | 60: ["C4", 261.63], 341 | 59: ["B3", 246.94], 342 | 58: ["A#3", 233.08], 343 | 57: ["A3", 220.00], 344 | 56: ["G#3", 207.65], 345 | 55: ["G3", 196.00], 346 | 54: ["F#3", 185.00], 347 | 53: ["F3", 174.61], 348 | 52: ["E3", 164.81], 349 | 51: ["D#3", 155.56], 350 | 50: ["D3", 146.83], 351 | 49: ["C#3", 138.59], 352 | 48: ["C3", 130.81], 353 | 47: ["B2", 123.47], 354 | 46: ["A#2", 116.54], 355 | 45: ["A2", 110.00], 356 | 44: ["G#2", 103.83], 357 | 43: ["G2", 98.00], 358 | 42: ["F#2", 92.50], 359 | 41: ["F2", 87.31], 360 | 40: ["E2", 82.41], 361 | 39: ["D#2", 77.78], 362 | 38: ["D2", 73.42], 363 | 37: ["C#2", 69.30], 364 | 36: ["C2", 65.41], 365 | 35: ["B1", 61.74], 366 | 34: ["A#1", 58.27], 367 | 33: ["A1", 55.00], 368 | 32: ["G#1", 51.91], 369 | 31: ["G1", 49.00], 370 | 30: ["F#1", 46.25], 371 | 29: ["F1", 43.65], 372 | 28: ["E1", 41.20], 373 | 27: ["D#1", 38.89], 374 | 26: ["D1", 36.71], 375 | 25: ["C#1", 34.65], 376 | 24: ["C1", 32.70], 377 | 23: ["B0", 30.87], 378 | 22: ["A#0", 29.14], 379 | 21: ["A0", 27.50], 380 | "G9": 127, 381 | "F#9": 126, 382 | "F9": 125, 383 | "E9": 124, 384 | "D#9": 123, 385 | "D9": 122, 386 | "C#9": 121, 387 | "C9": 120, 388 | "B8": 119, 389 | "A#8": 118, 390 | "A8": 117, 391 | "G#8": 116, 392 | "G8": 115, 393 | "F#8": 114, 394 | "F8": 113, 395 | "E8": 112, 396 | "D#8": 111, 397 | "D8": 110, 398 | "C#8": 109, 399 | "C8": 108, 400 | "B7": 107, 401 | "A#7": 106, 402 | "A7": 105, 403 | "G#7": 104, 404 | "G7": 103, 405 | "F#7": 102, 406 | "F7": 101, 407 | "E7": 100, 408 | "D#7": 99, 409 | "D7": 98, 410 | "C#7": 97, 411 | "C7": 96, 412 | "B6": 95, 413 | "A#6": 94, 414 | "A6": 93, 415 | "G#6": 92, 416 | "G6": 91, 417 | "F#6": 90, 418 | "F6": 89, 419 | "E6": 88, 420 | "D#6": 87, 421 | "D6": 86, 422 | "C#6": 85, 423 | "C6": 84, 424 | "B5": 83, 425 | "A#5": 82, 426 | "A5": 81, 427 | "G#5": 80, 428 | "G5": 79, 429 | "F#5": 78, 430 | "F5": 77, 431 | "E5": 76, 432 | "D#5": 75, 433 | "D5": 74, 434 | "C#5": 73, 435 | "C5": 72, 436 | "B4": 71, 437 | "A#4": 70, 438 | "A4": 69, 439 | "G#4": 68, 440 | "G4": 67, 441 | "F#4": 66, 442 | "F4": 65, 443 | "E4": 64, 444 | "D#4": 63, 445 | "D4": 62, 446 | "C#4": 61, 447 | "C4": 60, 448 | "B3": 59, 449 | "A#3": 58, 450 | "A3": 57, 451 | "G#3": 56, 452 | "G3": 55, 453 | "F#3": 54, 454 | "F3": 53, 455 | "E3": 52, 456 | "D#3": 51, 457 | "D3": 50, 458 | "C#3": 49, 459 | "C3": 48, 460 | "B2": 47, 461 | "A#2": 46, 462 | "A2": 45, 463 | "G#2": 44, 464 | "G2": 43, 465 | "F#2": 42, 466 | "F2": 41, 467 | "E2": 40, 468 | "D#2": 39, 469 | "D2": 38, 470 | "C#2": 37, 471 | "C2": 36, 472 | "B1": 35, 473 | "A#1": 34, 474 | "A1": 33, 475 | "G#1": 32, 476 | "G1": 31, 477 | "F#1": 30, 478 | "F1": 29, 479 | "E1": 28, 480 | "D#1": 27, 481 | "D1": 26, 482 | "C#1": 25, 483 | "C1": 24, 484 | "B0": 23, 485 | "A#0": 22, 486 | "A0": 21, 487 | } 488 | 489 | MIDI_QTR_FRAME_CODE = { 490 | 0: "24 fps", 491 | 1: "25 fps", 492 | 2: "29.97 fps", 493 | 3: "30 fps", 494 | "24 fps": 0, 495 | "25 fps": 1, 496 | "29.97 fps": 2, 497 | "30 fps": 3, 498 | } 499 | 500 | MIDI_SEQUENCE = { 501 | "NONE": None, 502 | "NOTE_OFF": 0x80, 503 | "NOTE_ON": 0x90, 504 | "POLY_AFTERTOUCH": 0xA0, 505 | "CONTROL_CHANGE": 0xB0, 506 | "PROGRAM_CHANGE": 0xC0, 507 | "CHANNEL_AFTERTOUCH": 0xD0, 508 | "PITCH_BEND": 0xE0, 509 | "SYSEX": 0xF0, 510 | "TIME_CODE_QTR_FRAME": 0xF1, 511 | "SONG_POSITION_POINTER": 0xF2, 512 | "SONG_SELECT": 0xF3, 513 | "TUNE_REQUEST": 0xF6, 514 | "SYSEX_END": 0xF7, 515 | "TIMING_CLOCK": 0xF8, 516 | "START": 0xFA, 517 | "CONTINUE": 0xFB, 518 | "STOP": 0xFC, 519 | "ACTIVE_SENSING": 0xFD, 520 | "RESET": 0xFF, 521 | } 522 | 523 | 524 | # Misc. methods ######################################################################################################## 525 | def percentage_to_7_bit(percentage: float) -> int: 526 | return int(percentage / 100 * 127) 527 | 528 | 529 | # Midi class ########################################################################################################### 530 | # noinspection PyTypeChecker 531 | class Midi: 532 | def __init__(self, uart, tx, rx): 533 | self.last_sequence = MIDI_SEQUENCE["NONE"] 534 | self.message = 0 535 | self.channel = 0 536 | self.state = 0 537 | self.last_rx_parameters = { 538 | "note_off": { 539 | "note": None, 540 | }, 541 | "note_on": { 542 | "note": None, 543 | "velocity": None, 544 | }, 545 | "poly_aftertouch": { 546 | "note": None, 547 | "pressure": None, 548 | }, 549 | "control_change": { 550 | "code": None, 551 | "value": None, 552 | }, 553 | "program_change": { 554 | "program": None, 555 | }, 556 | "channel_aftertouch": { 557 | "pressure": None, 558 | }, 559 | "pitch_bend": { 560 | "bend_LSB": None, 561 | "bend_MSB": None, 562 | "bend": None, 563 | }, 564 | "song_position": 0, 565 | "song_position_pointer": { 566 | "LSB": None, 567 | "MSB": None, 568 | "Position": None, 569 | }, 570 | "song_select": { 571 | "song": None, 572 | }, 573 | "time_code_qtr_frame": { 574 | "rate": None, 575 | "hours": None, 576 | "minutes": None, 577 | "seconds": None, 578 | "frames": None, 579 | }, 580 | "sysex_vendor_id": "", 581 | "sysex": [], 582 | } 583 | print("Starting MIDI...") 584 | self.uart = UART(uart, BAUD, tx=tx, rx=rx) 585 | print("Success!\t->OUTPUT <-INPUT") 586 | 587 | # UART "inherited" methods. C-based classes don't work when inherited in python ################################### 588 | def write(self, value: int): 589 | self.uart.write(bytes([value])) 590 | 591 | def read(self, n_bytes: int): 592 | return self.uart.read(n_bytes) 593 | 594 | def any(self): 595 | return self.uart.any() 596 | 597 | # Other ############################################################################################################ 598 | def get_parameter(self, message_type, parameter): 599 | return self.last_rx_parameters[message_type][parameter] 600 | 601 | # MIDI send methods ################################################################################################ 602 | def send_note_off(self, channel: int, note: int): 603 | self.write(MESSAGE_NOTE_OFF | channel) 604 | self.write(note) 605 | self.write(0) 606 | print(f"-> NOTE OFF\t\t{note} ({NOTE_CODE[note][0]})") 607 | 608 | def send_note_on(self, channel: int, note: int, velocity: int): 609 | self.write(MESSAGE_NOTE_ON | channel) 610 | self.write(note) 611 | self.write(velocity) 612 | print(f"-> NOTE ON\t\t{note} ({NOTE_CODE[note][0]})\t\tVELOCITY {velocity}") 613 | 614 | def send_poly_aftertouch(self, channel: int, note: int, pressure: int): 615 | self.write(MESSAGE_POLYPHONIC_AFTERTOUCH | channel) 616 | self.write(note) 617 | self.write(pressure) 618 | print(f"-> POLY AT\t\t{note} ({NOTE_CODE[note][0]})\t\t\tPRESSURE {pressure}") 619 | 620 | def send_control_change(self, channel: int, cc: int, value: int): 621 | self.write(MESSAGE_CONTROL_CHANGE | channel) 622 | self.write(cc) 623 | self.write(value) 624 | print(f"-> CONTROL CHANGE\t{cc} ({CONTROL_CHANGE_CODE[cc]})\tVALUE {value}") 625 | 626 | def send_program_change(self, channel: int, program: int): 627 | self.write(MESSAGE_PROGRAM_CHANGE | channel) 628 | self.write(program) 629 | print(f"-> PROGRAM CHANGE\t{channel} TO {program}") 630 | 631 | def send_channel_aftertouch(self, channel: int, amount: int): 632 | self.write(MESSAGE_CHANNEL_AFTERTOUCH | channel) 633 | self.write(amount) 634 | print(f"-> CHANNEL AT\t\tCHANNEL {channel+1}\t\tAMOUNT {amount}") 635 | 636 | def send_pitch_bend(self, channel: int, note: int, amount: int): 637 | self.write(MESSAGE_PITCH_BEND | channel) 638 | self.write(note) 639 | self.write(amount) 640 | print(f"-> PITCH BEND\t{note} AMOUNT {amount}") 641 | 642 | def send_sysex_start(self): 643 | self.write(MESSAGE_SYSTEM_EXCLUSIVE_START) 644 | print(f"-> SYSEX START") 645 | 646 | def send_time_code_qtr_frame(self, rate: int, hours: int, minutes: int, seconds: int, frames: int): 647 | self.write(MESSAGE_MIDI_TIME_CODE_QTR_FRAME) 648 | self.write((rate << 5) | hours) 649 | self.write(minutes) 650 | self.write(seconds) 651 | self.write(frames) 652 | print(f"-> TIME CODE\t\t{hours}:{minutes}:{seconds}.{frames}") 653 | 654 | def send_song_position_pointer(self, beats_since_start: int): 655 | lsb = beats_since_start & 0x7F 656 | msb = (beats_since_start >> 7) & 0x7F 657 | self.write(MESSAGE_SONG_POSITION_POINTER) 658 | self.write(lsb) 659 | self.write(msb) 660 | print(f"-> SONG POSITION\tBEAT {beats_since_start}") 661 | 662 | def send_song_select(self, song: int): 663 | self.write(MESSAGE_SONG_SELECT) 664 | self.write(song) 665 | print(f"-> SONG SELECT\t\tSONG {song}") 666 | 667 | def send_tune_request(self): 668 | self.write(MESSAGE_TUNE_REQUEST) 669 | print(f"-> TUNE REQUEST") 670 | 671 | def send_sysex(self, sysex_list: list): 672 | for item in sysex_list: 673 | self.write(item) 674 | 675 | def send_sysex_stop(self): 676 | self.write(MESSAGE_SYSTEM_EXCLUSIVE_STOP) 677 | print(f"-> SYSEX STOP") 678 | 679 | def send_timing_clock(self): 680 | self.write(MESSAGE_TIMING_CLOCK) 681 | print(f"-> TIMING CLOCK") 682 | 683 | def send_playback_start(self): 684 | self.write(MESSAGE_START) 685 | print(f"-> PLAYBACK START") 686 | 687 | def send_playback_continue(self): 688 | self.write(MESSAGE_CONTINUE) 689 | print(f"-> PLAYBACK CONTINUE") 690 | 691 | def send_playback_stop(self): 692 | self.write(MESSAGE_STOP) 693 | print(f"-> PLAYBACK STOP") 694 | 695 | def send_active_sensing(self): 696 | self.write(MESSAGE_ACTIVE_SENSING) 697 | print(f"-> ACTIVE SENSING") 698 | 699 | def send_reset(self): 700 | self.write(MESSAGE_SYSTEM_RESET) 701 | print(f"-> SYSTEM RESET") 702 | 703 | # MIDI receive methods ############################################################################################# 704 | def load_message(self, msg: int): 705 | self.message = msg 706 | self.analyze_message() 707 | 708 | def get_channel(self): 709 | return self.message & 0x0F 710 | 711 | def analyze_message(self): 712 | # ANYTHING GOES 713 | if self.state == 0: 714 | if self.message < 0xF0: 715 | self.state = self.message & 0xF0 716 | self.channel = self.get_channel() 717 | else: 718 | self.state = self.message 719 | # NOTE OFF 720 | elif self.state == MESSAGE_NOTE_OFF: 721 | self.last_rx_parameters["note_off"]["note"] = self.message 722 | self.state = 0x81 723 | elif self.state == 0x81: 724 | self.state = 0 725 | note_code = self.last_rx_parameters["note_off"]["note"] 726 | note_name = NOTE_CODE[self.last_rx_parameters["note_off"]["note"]][0] 727 | print(f"<- CH{self.channel+1} NOTE OFF\t\t{note_code} ({note_name})") 728 | self.last_sequence = MIDI_SEQUENCE["NOTE_OFF"] 729 | # NOTE ON 730 | elif self.state == MESSAGE_NOTE_ON: 731 | self.last_rx_parameters["note_on"]["note"] = self.message 732 | self.state = 0x91 733 | elif self.state == 0x91: 734 | self.last_rx_parameters["note_on"]["velocity"] = self.message 735 | self.state = 0 736 | note_code = self.last_rx_parameters["note_on"]["note"] 737 | note_name = NOTE_CODE[self.last_rx_parameters["note_on"]["note"]][0] 738 | velocity = self.last_rx_parameters["note_on"]['velocity'] 739 | print(f"<- CH{self.channel+1} NOTE ON\t\t{note_code} ({note_name})\t\t\tVELOCITY {velocity}") 740 | self.last_sequence = MIDI_SEQUENCE["NOTE_ON"] 741 | # POLY AFTERTOUCH 742 | elif self.state == MESSAGE_POLYPHONIC_AFTERTOUCH: 743 | self.last_rx_parameters["poly_aftertouch"]["note"] = self.message 744 | self.state = 0xA1 745 | elif self.state == 0xA1: 746 | self.last_rx_parameters["poly_aftertouch"]["pressure"] = self.message 747 | self.state = 0 748 | note_code = self.last_rx_parameters["poly_aftertouch"]["note"] 749 | note_name = NOTE_CODE[self.last_rx_parameters["poly_aftertouch"]["note"]][0] 750 | pressure = self.last_rx_parameters["poly_aftertouch"]["pressure"] 751 | print(f"<- CH{self.channel+1} POLY AFTERTOUCH\t\t{note_code} ({note_name})\t\tPRESSURE {pressure}") 752 | self.last_sequence = MIDI_SEQUENCE["POLY_AFTERTOUCH"] 753 | # CONTROL CHANGE 754 | elif self.state == MESSAGE_CONTROL_CHANGE: 755 | self.last_rx_parameters["control_change"]["code"] = self.message 756 | self.state = 0xB1 757 | elif self.state == 0xB1: 758 | self.last_rx_parameters["control_change"]["value"] = self.message 759 | self.state = 0 760 | cc_name = CONTROL_CHANGE_CODE[self.last_rx_parameters["control_change"]["code"]] 761 | cc_code = self.last_rx_parameters["control_change"]["code"] 762 | value = self.last_rx_parameters["control_change"]["value"] 763 | print(f"<- CH{self.channel+1} CONTROL CHANGE\t{cc_code} ({cc_name})\tVALUE {value}") 764 | self.last_sequence = MIDI_SEQUENCE["CONTROL_CHANGE"] 765 | # PROGRAM CHANGE 766 | elif self.state == MESSAGE_PROGRAM_CHANGE: 767 | self.last_rx_parameters["program_change"]["program"] = self.message 768 | self.state = 0 769 | program = self.last_rx_parameters["program_change"]["program"] 770 | print(f"<- CH{self.channel + 1} PROGRAM CHANGE\t{program}") 771 | self.last_sequence = MIDI_SEQUENCE["PROGRAM_CHANGE"] 772 | # CHANNEL AFTERTOUCH 773 | elif self.state == MESSAGE_CHANNEL_AFTERTOUCH: 774 | self.last_rx_parameters["channel_aftertouch"]["pressure"] = self.message 775 | self.state = 0 776 | pressure = self.last_rx_parameters["channel_aftertouch"]["pressure"] 777 | print(f"<- CH{self.channel + 1} CHANNEL AFTERTOUCH\t\t\tPRESSURE {pressure}") 778 | self.last_sequence = MIDI_SEQUENCE["CHANNEL_AFTERTOUCH"] 779 | # PITCH BEND 780 | elif self.state == MESSAGE_PITCH_BEND: 781 | self.last_rx_parameters["pitch_bend"]["bend_LSB"] = self.message 782 | self.state = 0xE1 783 | elif self.state == 0xE1: 784 | self.last_rx_parameters["pitch_bend"]["bend_MSB"] = self.message 785 | bend_msb = self.last_rx_parameters["pitch_bend"]["bend_MSB"] 786 | bend_lsb = self.last_rx_parameters["pitch_bend"]["bend_LSB"] 787 | self.state = 0 788 | self.last_rx_parameters["pitch_bend"]["bend"] = (bend_msb << 8) | bend_lsb 789 | bend = self.last_rx_parameters["pitch_bend"]["bend"] 790 | print(f"<- CH{self.channel+1} PITCH BEND\t\tBEND {bend}") 791 | self.last_sequence = MIDI_SEQUENCE["PITCH_BEND"] 792 | # START 793 | elif self.state == MESSAGE_START: 794 | self.state = 0 795 | print(f"<- START") 796 | self.last_sequence = MIDI_SEQUENCE["START"] 797 | # STOP 798 | elif self.state == MESSAGE_STOP: 799 | self.state = 0 800 | print(f"<- STOP") 801 | self.last_sequence = MIDI_SEQUENCE["STOP"] 802 | # CONTINUE 803 | elif self.state == MESSAGE_CONTINUE: 804 | self.state = 0 805 | print(f"<- CONTINUE") 806 | self.last_sequence = MIDI_SEQUENCE["CONTINUE"] 807 | # TIMING CLOCK 808 | elif self.state == MESSAGE_TIMING_CLOCK: 809 | self.state = 0 810 | print(f"<- TIMING CLOCK") 811 | self.last_sequence = MIDI_SEQUENCE["TIMING_CLOCK"] 812 | # SYSEX START 813 | elif self.state == MESSAGE_START: 814 | self.state = 0xF4 815 | print(f"<- SYSEX START") 816 | self.last_sequence = MIDI_SEQUENCE["SYSEX"] 817 | self.last_rx_parameters["sysex_vendor_id"] = self.message 818 | # SYSEX SEQUENCE 819 | elif self.state == MESSAGE_SYSTEM_EXCLUSIVE_START: 820 | if self.message != MESSAGE_SYSTEM_EXCLUSIVE_STOP: 821 | self.last_rx_parameters["sysex"].append(self.message) 822 | print(f"<- SYSEX: {hex(self.message)}") 823 | else: 824 | self.state = 0 825 | print(f"<- SYSEX STOP") 826 | self.last_sequence = MIDI_SEQUENCE["SYSEX_END"] 827 | # TUNE REQUEST 828 | elif self.state == MESSAGE_TUNE_REQUEST: 829 | self.state = 0 830 | print(f"<- TUNE REQUEST") 831 | self.last_sequence = MIDI_SEQUENCE["TUNE_REQUEST"] 832 | # SONG SELECT 833 | elif self.state == MESSAGE_SONG_SELECT: 834 | self.last_rx_parameters["song_select"]["song"] = self.message 835 | self.state = 0 836 | song = self.last_rx_parameters["song_select"]["song"] 837 | print(f"<- SONG SELECT\t\t{song}") 838 | self.last_sequence = MIDI_SEQUENCE["SONG_SELECT"] 839 | # ACTIVE SENSING 840 | elif self.state == MESSAGE_ACTIVE_SENSING: 841 | self.state = 0 842 | print(f"<- ACTIVE SENSING") 843 | self.last_sequence = MIDI_SEQUENCE["ACTIVE_SENSING"] 844 | # SYSTEM RESET 845 | elif self.state == MESSAGE_SYSTEM_RESET: 846 | self.state = 0 847 | print(f"<- SYSTEM RESET") 848 | self.last_sequence = MIDI_SEQUENCE["SYSTEM_RESET"] 849 | # MIDI TIME CODE QTR FRAME 850 | elif self.state == 0xF1: 851 | self.state = 0xF10 852 | rate = (0b01100000 | self.message) >> 5 853 | self.last_rx_parameters["time_code_qtr_frame"]["rate"] = MIDI_QTR_FRAME_CODE[rate] 854 | hour = (0b00011111 | self.message) 855 | self.last_rx_parameters["time_code_qtr_frame"]["hour"] = hour 856 | elif self.state == 0xF10: 857 | self.state = 0xF11 858 | minute = self.message 859 | self.last_rx_parameters["time_code_qtr_frame"]["minute"] = minute 860 | elif self.state == 0xF11: 861 | self.state = 0xF12 862 | second = self.message 863 | self.last_rx_parameters["time_code_qtr_frame"]["second"] = second 864 | elif self.state == 0xF12: 865 | self.state = 0 866 | frame = self.message 867 | self.last_rx_parameters["time_code_qtr_frame"]["frame"] = frame 868 | rate = self.last_rx_parameters["time_code_qtr_frame"]["rate"] 869 | hour = self.last_rx_parameters["time_code_qtr_frame"]["hour"] 870 | second = self.last_rx_parameters["time_code_qtr_frame"]["second"] 871 | minute = self.last_rx_parameters["time_code_qtr_frame"]["minute"] 872 | print(f"<- TIME CODE QTR FRAME\t{rate}\t{hour}:{minute}:{second}.{frame}") 873 | self.last_sequence = MIDI_SEQUENCE["TIME_CODE_QTR_FRAME"] 874 | # SONG POSITION POINTER 875 | elif self.state == 0xF2: 876 | self.last_rx_parameters["song_position_pointer"]["LSB"] = self.message 877 | self.state = 0xF9 878 | elif self.state == 0xF9: 879 | self.state = 0 880 | self.last_rx_parameters["song_position_pointer"]["MSB"] = self.message 881 | msb = self.last_rx_parameters["song_position_pointer"]["MSB"] 882 | lsb = self.last_rx_parameters["song_position_pointer"]["LSB"] 883 | position = (msb << 8) | lsb 884 | self.last_rx_parameters["song_position_pointer"]["position"] = position 885 | print(f"<- SONG POSITION POINTER {position}") 886 | self.last_sequence = MIDI_SEQUENCE["SONG_POSITION_POINTER"] 887 | # OTHER (THIS SHOULD NEVER HAPPEN) 888 | else: 889 | self.state = 0 890 | print("<- NOT DEFINED") 891 | 892 | --------------------------------------------------------------------------------