├── docs ├── _static │ ├── favicon.ico │ ├── favicon.ico.license │ └── custom.css ├── api.rst.license ├── examples.rst.license ├── index.rst.license ├── requirements.txt ├── examples.rst ├── index.rst ├── api.rst └── conf.py ├── README.rst.license ├── optional_requirements.txt ├── requirements.txt ├── .gitattributes ├── .github ├── workflows │ ├── build.yml │ ├── release_pypi.yml │ ├── release_gh.yml │ └── failure-help-text.yml └── PULL_REQUEST_TEMPLATE │ └── adafruit_circuitpython_pr.md ├── .readthedocs.yaml ├── .pre-commit-config.yaml ├── adafruit_midi ├── stop.py ├── start.py ├── midi_continue.py ├── active_sensing.py ├── timing_clock.py ├── program_change.py ├── channel_pressure.py ├── control_change.py ├── pitch_bend.py ├── polyphonic_key_pressure.py ├── system_exclusive.py ├── note_on.py ├── note_off.py ├── mtc_quarter_frame.py ├── control_change_values.py ├── __init__.py └── midi_message.py ├── examples ├── midi_intest1.py ├── midi_simpletest.py ├── midi_memorycheck.py └── midi_inoutdemo.py ├── LICENSES ├── MIT.txt ├── Unlicense.txt └── CC-BY-4.0.txt ├── LICENSE ├── pyproject.toml ├── .gitignore ├── tests ├── test_note_parser.py ├── test_MIDIMessage_unittests.py └── test_MIDI_unittests.py ├── README.rst ├── ruff.toml └── CODE_OF_CONDUCT.md /docs/_static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/Adafruit_CircuitPython_MIDI/HEAD/docs/_static/favicon.ico -------------------------------------------------------------------------------- /docs/api.rst.license: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 ladyada for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | -------------------------------------------------------------------------------- /docs/examples.rst.license: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 ladyada for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | -------------------------------------------------------------------------------- /docs/index.rst.license: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 ladyada for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | -------------------------------------------------------------------------------- /README.rst.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries 2 | 3 | SPDX-License-Identifier: MIT 4 | -------------------------------------------------------------------------------- /optional_requirements.txt: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Alec Delaney, for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | -------------------------------------------------------------------------------- /docs/_static/favicon.ico.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2018 Phillip Torrone for Adafruit Industries 2 | 3 | SPDX-License-Identifier: CC-BY-4.0 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Alec Delaney, for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | Adafruit-Blinka 6 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Kattni Rembor for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | sphinx 6 | sphinxcontrib-jquery 7 | sphinx-rtd-theme 8 | -------------------------------------------------------------------------------- /docs/examples.rst: -------------------------------------------------------------------------------- 1 | Simple test 2 | ------------ 3 | 4 | Ensure your device works with this simple test. 5 | 6 | .. literalinclude:: ../examples/midi_simpletest.py 7 | :caption: examples/midi_simpletest.py 8 | :linenos: 9 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Justin Myers for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | .py text eol=lf 6 | .rst text eol=lf 7 | .txt text eol=lf 8 | .yaml text eol=lf 9 | .toml text eol=lf 10 | .license text eol=lf 11 | .md text eol=lf 12 | -------------------------------------------------------------------------------- /docs/_static/custom.css: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2025 Sam Blenny 2 | * SPDX-License-Identifier: MIT 3 | */ 4 | 5 | /* Monkey patch the rtd theme to prevent horizontal stacking of short items 6 | * see https://github.com/readthedocs/sphinx_rtd_theme/issues/1301 7 | */ 8 | .py.property{display: block !important;} 9 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | name: Build CI 6 | 7 | on: [pull_request, push] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Run Build CI workflow 14 | uses: adafruit/workflows-circuitpython-libs/build@main 15 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | # Read the Docs configuration file 6 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 7 | 8 | # Required 9 | version: 2 10 | 11 | sphinx: 12 | configuration: docs/conf.py 13 | 14 | build: 15 | os: ubuntu-lts-latest 16 | tools: 17 | python: "3" 18 | 19 | python: 20 | install: 21 | - requirements: docs/requirements.txt 22 | - requirements: requirements.txt 23 | -------------------------------------------------------------------------------- /.github/workflows/release_pypi.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | name: PyPI Release Actions 6 | 7 | on: 8 | release: 9 | types: [published] 10 | 11 | jobs: 12 | upload-release-assets: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Run PyPI Release CI workflow 16 | uses: adafruit/workflows-circuitpython-libs/release-pypi@main 17 | with: 18 | pypi-username: ${{ secrets.pypi_username }} 19 | pypi-password: ${{ secrets.pypi_password }} 20 | -------------------------------------------------------------------------------- /.github/workflows/release_gh.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | name: GitHub Release Actions 6 | 7 | on: 8 | release: 9 | types: [published] 10 | 11 | jobs: 12 | upload-release-assets: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Run GitHub Release CI workflow 16 | uses: adafruit/workflows-circuitpython-libs/release-gh@main 17 | with: 18 | github-token: ${{ secrets.GITHUB_TOKEN }} 19 | upload-url: ${{ github.event.release.upload_url }} 20 | -------------------------------------------------------------------------------- /.github/workflows/failure-help-text.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Scott Shawcroft for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | name: Failure help text 6 | 7 | on: 8 | workflow_run: 9 | workflows: ["Build CI"] 10 | types: 11 | - completed 12 | 13 | jobs: 14 | post-help: 15 | runs-on: ubuntu-latest 16 | if: ${{ github.event.workflow_run.conclusion == 'failure' && github.event.workflow_run.event == 'pull_request' }} 17 | steps: 18 | - name: Post comment to help 19 | uses: adafruit/circuitpython-action-library-ci-failed@v1 20 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Justin Myers for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | repos: 6 | - repo: https://github.com/pre-commit/pre-commit-hooks 7 | rev: v4.5.0 8 | hooks: 9 | - id: check-yaml 10 | - id: end-of-file-fixer 11 | - id: trailing-whitespace 12 | - repo: https://github.com/astral-sh/ruff-pre-commit 13 | rev: v0.3.4 14 | hooks: 15 | - id: ruff-format 16 | - id: ruff 17 | args: ["--fix"] 18 | - repo: https://github.com/fsfe/reuse-tool 19 | rev: v3.0.1 20 | hooks: 21 | - id: reuse 22 | -------------------------------------------------------------------------------- /adafruit_midi/stop.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 Kevin J. Walters for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | """ 6 | `adafruit_midi.stop` 7 | ================================================================================ 8 | 9 | Stop MIDI message. 10 | 11 | 12 | * Author(s): Kevin J. Walters 13 | 14 | Implementation Notes 15 | -------------------- 16 | 17 | """ 18 | 19 | from .midi_message import MIDIMessage 20 | 21 | __version__ = "0.0.0+auto.0" 22 | __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MIDI.git" 23 | 24 | 25 | class Stop(MIDIMessage): 26 | """Stop MIDI message.""" 27 | 28 | _STATUS = 0xFC 29 | _STATUSMASK = 0xFF 30 | LENGTH = 1 31 | _message_slots = [] 32 | 33 | 34 | Stop.register_message_type() 35 | -------------------------------------------------------------------------------- /adafruit_midi/start.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 Kevin J. Walters for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | """ 6 | `adafruit_midi.start` 7 | ================================================================================ 8 | 9 | Start MIDI message. 10 | 11 | 12 | * Author(s): Kevin J. Walters 13 | 14 | Implementation Notes 15 | -------------------- 16 | 17 | """ 18 | 19 | from .midi_message import MIDIMessage 20 | 21 | __version__ = "0.0.0+auto.0" 22 | __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MIDI.git" 23 | 24 | 25 | class Start(MIDIMessage): 26 | """Start MIDI message.""" 27 | 28 | _STATUS = 0xFA 29 | _STATUSMASK = 0xFF 30 | LENGTH = 1 31 | _message_slots = [] 32 | 33 | 34 | Start.register_message_type() 35 | -------------------------------------------------------------------------------- /adafruit_midi/midi_continue.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 Kevin J. Walters for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | """ 6 | `adafruit_midi.midi_continue` 7 | ================================================================================ 8 | 9 | Continue MIDI message. 10 | 11 | 12 | * Author(s): Mark Komus 13 | 14 | Implementation Notes 15 | -------------------- 16 | 17 | """ 18 | 19 | from .midi_message import MIDIMessage 20 | 21 | __version__ = "0.0.0+auto.0" 22 | __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MIDI.git" 23 | 24 | 25 | class Continue(MIDIMessage): 26 | """Continue MIDI message.""" 27 | 28 | _message_slots = [] 29 | 30 | _STATUS = 0xFB 31 | _STATUSMASK = 0xFF 32 | LENGTH = 1 33 | 34 | 35 | Continue.register_message_type() 36 | -------------------------------------------------------------------------------- /examples/midi_intest1.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries 2 | # SPDX-License-Identifier: MIT 3 | 4 | import time 5 | 6 | import usb_midi 7 | 8 | import adafruit_midi 9 | 10 | # from adafruit_midi.channel_pressure import ChannelPressure 11 | from adafruit_midi.control_change import ControlChange 12 | from adafruit_midi.note_off import NoteOff 13 | from adafruit_midi.note_on import NoteOn 14 | from adafruit_midi.pitch_bend import PitchBend 15 | from adafruit_midi.timing_clock import TimingClock 16 | 17 | # 0 is MIDI channel 1 18 | midi = adafruit_midi.MIDI(midi_in=usb_midi.ports[0], in_channel=0) 19 | 20 | print("Midi input test") 21 | 22 | # Convert channel numbers at the presentation layer to the ones musicians use 23 | print("Input channel:", midi.in_channel + 1) 24 | 25 | while True: 26 | msg = midi.receive() 27 | if msg is not None: 28 | print(time.monotonic(), msg) 29 | -------------------------------------------------------------------------------- /adafruit_midi/active_sensing.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Matthew Badeau 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | """ 6 | `adafruit_midi.active_sensing` 7 | ================================================================================ 8 | 9 | Active Sensing MIDI message. 10 | 11 | 12 | * Author(s): Matthew Badeau 13 | 14 | Implementation Notes 15 | -------------------- 16 | 17 | """ 18 | 19 | from .midi_message import MIDIMessage 20 | 21 | __version__ = "0.0.0+auto.0" 22 | __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MIDI.git" 23 | 24 | 25 | class ActiveSensing(MIDIMessage): 26 | """Active Sensing MIDI message. 27 | 28 | Active Sensing message is a keepalive message sent every 300 milliseconds 29 | to tell the bus that the session is still good and alive. 30 | """ 31 | 32 | _STATUS = 0xFE 33 | _STATUSMASK = 0xFF 34 | LENGTH = 1 35 | _slots = [] 36 | 37 | 38 | ActiveSensing.register_message_type() 39 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/adafruit_circuitpython_pr.md: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | Thank you for contributing! Before you submit a pull request, please read the following. 6 | 7 | Make sure any changes you're submitting are in line with the CircuitPython Design Guide, available here: https://docs.circuitpython.org/en/latest/docs/design_guide.html 8 | 9 | If your changes are to documentation, please verify that the documentation builds locally by following the steps found here: https://adafru.it/build-docs 10 | 11 | Before submitting the pull request, make sure you've run Pylint and Black locally on your code. You can do this manually or using pre-commit. Instructions are available here: https://adafru.it/check-your-code 12 | 13 | Please remove all of this text before submitting. Include an explanation or list of changes included in your PR, as well as, if applicable, a link to any related issues. 14 | -------------------------------------------------------------------------------- /adafruit_midi/timing_clock.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 Kevin J. Walters for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | """ 6 | `adafruit_midi.timing_clock` 7 | ================================================================================ 8 | 9 | Timing Clock MIDI message. 10 | 11 | 12 | * Author(s): Kevin J. Walters 13 | 14 | Implementation Notes 15 | -------------------- 16 | 17 | """ 18 | 19 | from .midi_message import MIDIMessage 20 | 21 | __version__ = "0.0.0+auto.0" 22 | __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MIDI.git" 23 | 24 | 25 | # Good to have this registered first as it occurs frequently when present 26 | class TimingClock(MIDIMessage): 27 | """Timing Clock MIDI message. 28 | 29 | This occurs 24 times per quarter note when synchronization is in use. 30 | If this is not needed it's best to avoid this sending this high frequency 31 | message to a CircuitPython device to reduce the amount of message processing. 32 | """ 33 | 34 | _STATUS = 0xF8 35 | _STATUSMASK = 0xFF 36 | LENGTH = 1 37 | _slots = [] 38 | 39 | 40 | TimingClock.register_message_type() 41 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | 3 | Table of Contents 4 | ================= 5 | 6 | .. toctree:: 7 | :maxdepth: 4 8 | :hidden: 9 | 10 | self 11 | 12 | .. toctree:: 13 | :caption: Examples 14 | 15 | examples 16 | 17 | .. toctree:: 18 | :caption: API Reference 19 | :maxdepth: 3 20 | 21 | api 22 | 23 | .. toctree:: 24 | :caption: Tutorials 25 | 26 | .. toctree:: 27 | :caption: Related Products 28 | 29 | .. toctree:: 30 | :caption: Other Links 31 | 32 | Download from GitHub 33 | Download Library Bundle 34 | CircuitPython Reference Documentation 35 | CircuitPython Support Forum 36 | Discord Chat 37 | Adafruit Learning System 38 | Adafruit Blog 39 | Adafruit Store 40 | 41 | Indices and tables 42 | ================== 43 | 44 | * :ref:`genindex` 45 | * :ref:`modindex` 46 | * :ref:`search` 47 | -------------------------------------------------------------------------------- /LICENSES/MIT.txt: -------------------------------------------------------------------------------- 1 | MIT License Copyright (c) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice (including the next 11 | paragraph) shall be included in all copies or substantial portions of the 12 | Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 17 | OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 18 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF 19 | OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Ladyada for Adafruit Industries, Kevin J. Walters 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 | -------------------------------------------------------------------------------- /examples/midi_simpletest.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries 2 | # SPDX-License-Identifier: MIT 3 | # simple_test 4 | import random 5 | import time 6 | 7 | import usb_midi 8 | 9 | import adafruit_midi 10 | from adafruit_midi.control_change import ControlChange 11 | from adafruit_midi.note_off import NoteOff 12 | from adafruit_midi.note_on import NoteOn 13 | from adafruit_midi.pitch_bend import PitchBend 14 | 15 | print(usb_midi.ports) 16 | midi = adafruit_midi.MIDI( 17 | midi_in=usb_midi.ports[0], in_channel=0, midi_out=usb_midi.ports[1], out_channel=0 18 | ) 19 | print("Midi test") 20 | # Convert channel numbers at the presentation layer to the ones musicians use 21 | print("Default output channel:", midi.out_channel + 1) 22 | print("Listening on input channel:", midi.in_channel + 1) 23 | while True: 24 | midi.send(NoteOn(44, 120)) # G sharp 2nd octave 25 | time.sleep(0.25) 26 | a_pitch_bend = PitchBend(random.randint(0, 16383)) 27 | midi.send(a_pitch_bend) 28 | # note how a list of messages can be used 29 | midi.send([NoteOff("G#2", 120), ControlChange(3, 44)]) 30 | time.sleep(0.5) 31 | msg = midi.receive() 32 | if msg is not None: 33 | print("Received:", msg, "at", time.monotonic()) 34 | -------------------------------------------------------------------------------- /LICENSES/Unlicense.txt: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or distribute 4 | this software, either in source code form or as a compiled binary, for any 5 | purpose, commercial or non-commercial, and by any means. 6 | 7 | In jurisdictions that recognize copyright laws, the author or authors of this 8 | software dedicate any and all copyright interest in the software to the public 9 | domain. We make this dedication for the benefit of the public at large and 10 | to the detriment of our heirs and successors. We intend this dedication to 11 | be an overt act of relinquishment in perpetuity of all present and future 12 | rights to this software under copyright law. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 17 | BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH 19 | THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. For more information, 20 | please refer to 21 | -------------------------------------------------------------------------------- /adafruit_midi/program_change.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 Kevin J. Walters for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | """ 6 | `adafruit_midi.program_change` 7 | ================================================================================ 8 | 9 | Program Change MIDI message. 10 | 11 | 12 | * Author(s): Kevin J. Walters 13 | 14 | Implementation Notes 15 | -------------------- 16 | 17 | """ 18 | 19 | from .midi_message import MIDIMessage 20 | 21 | __version__ = "0.0.0+auto.0" 22 | __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MIDI.git" 23 | 24 | 25 | class ProgramChange(MIDIMessage): 26 | """Program Change MIDI message. 27 | 28 | :param int patch: The new program/patch number to use, 0-127. 29 | """ 30 | 31 | _message_slots = ["patch", "channel"] 32 | _STATUS = 0xC0 33 | _STATUSMASK = 0xF0 34 | LENGTH = 2 35 | 36 | def __init__(self, patch, *, channel=None): 37 | self.patch = patch 38 | super().__init__(channel=channel) 39 | if not 0 <= self.patch <= 127: 40 | self._raise_valueerror_oor() 41 | 42 | def __bytes__(self): 43 | return bytes([self._STATUS | (self.channel & self.CHANNELMASK), self.patch]) 44 | 45 | @classmethod 46 | def from_bytes(cls, msg_bytes): 47 | return cls(msg_bytes[1], channel=msg_bytes[0] & cls.CHANNELMASK) 48 | 49 | 50 | ProgramChange.register_message_type() 51 | -------------------------------------------------------------------------------- /adafruit_midi/channel_pressure.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 Kevin J. Walters for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | """ 6 | `adafruit_midi.channel_pressure` 7 | ================================================================================ 8 | 9 | Channel Pressure MIDI message. 10 | 11 | 12 | * Author(s): Kevin J. Walters 13 | 14 | Implementation Notes 15 | -------------------- 16 | 17 | """ 18 | 19 | from .midi_message import MIDIMessage 20 | 21 | __version__ = "0.0.0+auto.0" 22 | __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MIDI.git" 23 | 24 | 25 | class ChannelPressure(MIDIMessage): 26 | """Channel Pressure MIDI message. 27 | 28 | :param int pressure: The pressure, 0-127. 29 | """ 30 | 31 | _message_slots = ["pressure", "channel"] 32 | _STATUS = 0xD0 33 | _STATUSMASK = 0xF0 34 | LENGTH = 2 35 | 36 | def __init__(self, pressure, *, channel=None): 37 | self.pressure = pressure 38 | super().__init__(channel=channel) 39 | if not 0 <= self.pressure <= 127: 40 | self._raise_valueerror_oor() 41 | 42 | def __bytes__(self): 43 | return bytes([self._STATUS | (self.channel & self.CHANNELMASK), self.pressure]) 44 | 45 | @classmethod 46 | def from_bytes(cls, msg_bytes): 47 | return cls(msg_bytes[1], channel=msg_bytes[0] & cls.CHANNELMASK) 48 | 49 | 50 | ChannelPressure.register_message_type() 51 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Alec Delaney for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | [build-system] 6 | requires = [ 7 | "setuptools", 8 | "wheel", 9 | "setuptools-scm", 10 | ] 11 | 12 | [project] 13 | name = "adafruit-circuitpython-midi" 14 | description = "A CircuitPython helper for encoding/decoding MIDI packets over a MIDI or UART connection." 15 | version = "0.0.0+auto.0" 16 | readme = "README.rst" 17 | authors = [ 18 | {name = "Adafruit Industries", email = "circuitpython@adafruit.com"} 19 | ] 20 | urls = {Homepage = "https://github.com/adafruit/Adafruit_CircuitPython_MIDI"} 21 | keywords = [ 22 | "adafruit", 23 | "blinka", 24 | "circuitpython", 25 | "micropython", 26 | "midi", 27 | "uart", 28 | "encoding", 29 | "decoding", 30 | ] 31 | license = {text = "MIT"} 32 | classifiers = [ 33 | "Intended Audience :: Developers", 34 | "Topic :: Software Development :: Libraries", 35 | "Topic :: Software Development :: Embedded Systems", 36 | "Topic :: System :: Hardware", 37 | "License :: OSI Approved :: MIT License", 38 | "Programming Language :: Python :: 3", 39 | ] 40 | dynamic = ["dependencies", "optional-dependencies"] 41 | 42 | [tool.setuptools] 43 | packages = ["adafruit_midi"] 44 | 45 | [tool.setuptools.dynamic] 46 | dependencies = {file = ["requirements.txt"]} 47 | optional-dependencies = {optional = {file = ["optional_requirements.txt"]}} 48 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | 2 | .. If you created a package, create one automodule per module in the package. 3 | 4 | .. If your library file(s) are nested in a directory (e.g. /adafruit_foo/foo.py) 5 | .. use this format as the module name: "adafruit_foo.foo" 6 | 7 | API Reference 8 | ############# 9 | 10 | .. automodule:: adafruit_midi 11 | :members: 12 | 13 | .. automodule:: adafruit_midi.active_sensing 14 | :members: 15 | 16 | .. automodule:: adafruit_midi.channel_pressure 17 | :members: 18 | 19 | .. automodule:: adafruit_midi.control_change 20 | :members: 21 | 22 | .. automodule:: adafruit_midi.control_change_values 23 | :members: 24 | 25 | .. automodule:: adafruit_midi.midi_continue 26 | :members: 27 | 28 | .. automodule:: adafruit_midi.midi_message 29 | :members: 30 | 31 | .. automodule:: adafruit_midi.mtc_quarter_frame 32 | :members: 33 | 34 | .. automodule:: adafruit_midi.note_off 35 | :members: 36 | 37 | .. automodule:: adafruit_midi.note_on 38 | :members: 39 | 40 | .. automodule:: adafruit_midi.pitch_bend 41 | :members: 42 | 43 | .. automodule:: adafruit_midi.polyphonic_key_pressure 44 | :members: 45 | 46 | .. automodule:: adafruit_midi.program_change 47 | :members: 48 | 49 | .. automodule:: adafruit_midi.start 50 | :members: 51 | 52 | .. automodule:: adafruit_midi.stop 53 | :members: 54 | 55 | .. automodule:: adafruit_midi.system_exclusive 56 | :members: 57 | 58 | .. automodule:: adafruit_midi.timing_clock 59 | :members: 60 | -------------------------------------------------------------------------------- /adafruit_midi/control_change.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 Kevin J. Walters for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | """ 6 | `adafruit_midi.control_change` 7 | ================================================================================ 8 | 9 | Control Change MIDI message. 10 | 11 | 12 | * Author(s): Kevin J. Walters 13 | 14 | Implementation Notes 15 | -------------------- 16 | 17 | """ 18 | 19 | from .midi_message import MIDIMessage 20 | 21 | __version__ = "0.0.0+auto.0" 22 | __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MIDI.git" 23 | 24 | 25 | class ControlChange(MIDIMessage): 26 | """Control Change MIDI message. 27 | 28 | :param int control: The control number, 0-127. 29 | :param int value: The 7bit value of the control, 0-127. 30 | 31 | """ 32 | 33 | _message_slots = ["control", "value", "channel"] 34 | _STATUS = 0xB0 35 | _STATUSMASK = 0xF0 36 | LENGTH = 3 37 | 38 | def __init__(self, control, value, *, channel=None): 39 | self.control = control 40 | self.value = value 41 | super().__init__(channel=channel) 42 | if not 0 <= self.control <= 127 or not 0 <= self.value <= 127: 43 | self._raise_valueerror_oor() 44 | 45 | def __bytes__(self): 46 | return bytes([self._STATUS | (self.channel & self.CHANNELMASK), self.control, self.value]) 47 | 48 | @classmethod 49 | def from_bytes(cls, msg_bytes): 50 | return cls(msg_bytes[1], msg_bytes[2], channel=msg_bytes[0] & cls.CHANNELMASK) 51 | 52 | 53 | ControlChange.register_message_type() 54 | -------------------------------------------------------------------------------- /adafruit_midi/pitch_bend.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 Kevin J. Walters for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | """ 6 | `adafruit_midi.pitch_bend` 7 | ================================================================================ 8 | 9 | Pitch Bend Change MIDI message. 10 | 11 | 12 | * Author(s): Kevin J. Walters 13 | 14 | Implementation Notes 15 | -------------------- 16 | 17 | """ 18 | 19 | from .midi_message import MIDIMessage 20 | 21 | __version__ = "0.0.0+auto.0" 22 | __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MIDI.git" 23 | 24 | 25 | class PitchBend(MIDIMessage): 26 | """Pitch Bend Change MIDI message. 27 | 28 | :param int pitch_bend: A 14bit unsigned int representing the degree of 29 | bend from 0 through 8192 (midpoint, no bend) to 16383. 30 | """ 31 | 32 | _message_slots = ["pitch_bend", "channel"] 33 | _STATUS = 0xE0 34 | _STATUSMASK = 0xF0 35 | LENGTH = 3 36 | 37 | def __init__(self, pitch_bend, *, channel=None): 38 | self.pitch_bend = pitch_bend 39 | super().__init__(channel=channel) 40 | if not 0 <= self.pitch_bend <= 16383: 41 | self._raise_valueerror_oor() 42 | 43 | def __bytes__(self): 44 | return bytes( 45 | [ 46 | self._STATUS | (self.channel & self.CHANNELMASK), 47 | self.pitch_bend & 0x7F, 48 | (self.pitch_bend >> 7) & 0x7F, 49 | ] 50 | ) 51 | 52 | @classmethod 53 | def from_bytes(cls, msg_bytes): 54 | return cls(msg_bytes[2] << 7 | msg_bytes[1], channel=msg_bytes[0] & cls.CHANNELMASK) 55 | 56 | 57 | PitchBend.register_message_type() 58 | -------------------------------------------------------------------------------- /adafruit_midi/polyphonic_key_pressure.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 Kevin J. Walters for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | """ 6 | `adafruit_midi.polyphonic_key_pressure` 7 | ================================================================================ 8 | 9 | Polyphonic Key Pressure MIDI message. 10 | 11 | 12 | * Author(s): Kevin J. Walters 13 | 14 | Implementation Notes 15 | -------------------- 16 | 17 | """ 18 | 19 | from .midi_message import MIDIMessage, note_parser 20 | 21 | __version__ = "0.0.0+auto.0" 22 | __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MIDI.git" 23 | 24 | 25 | class PolyphonicKeyPressure(MIDIMessage): 26 | """Polyphonic Key Pressure MIDI message. 27 | 28 | :param note: The note (key) number either as an ``int`` (0-127) or a 29 | ``str`` which is parsed, e.g. "C4" (middle C) is 60, "A4" is 69. 30 | :param int pressure: The pressure, 0-127. 31 | """ 32 | 33 | _message_slots = ["note", "pressure", "channel"] 34 | _STATUS = 0xA0 35 | _STATUSMASK = 0xF0 36 | LENGTH = 3 37 | 38 | def __init__(self, note, pressure, *, channel=None): 39 | self.note = note_parser(note) 40 | self.pressure = pressure 41 | super().__init__(channel=channel) 42 | if not 0 <= self.note <= 127 or not 0 <= self.pressure <= 127: 43 | self._raise_valueerror_oor() 44 | 45 | def __bytes__(self): 46 | return bytes([self._STATUS | (self.channel & self.CHANNELMASK), self.note, self.pressure]) 47 | 48 | @classmethod 49 | def from_bytes(cls, msg_bytes): 50 | return cls(msg_bytes[1], msg_bytes[2], channel=msg_bytes[0] & cls.CHANNELMASK) 51 | 52 | 53 | PolyphonicKeyPressure.register_message_type() 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Kattni Rembor, written for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | # Do not include files and directories created by your personal work environment, such as the IDE 6 | # you use, except for those already listed here. Pull requests including changes to this file will 7 | # not be accepted. 8 | 9 | # This .gitignore file contains rules for files generated by working with CircuitPython libraries, 10 | # including building Sphinx, testing with pip, and creating a virual environment, as well as the 11 | # MacOS and IDE-specific files generated by using MacOS in general, or the PyCharm or VSCode IDEs. 12 | 13 | # If you find that there are files being generated on your machine that should not be included in 14 | # your git commit, you should create a .gitignore_global file on your computer to include the 15 | # files created by your personal setup. To do so, follow the two steps below. 16 | 17 | # First, create a file called .gitignore_global somewhere convenient for you, and add rules for 18 | # the files you want to exclude from git commits. 19 | 20 | # Second, configure Git to use the exclude file for all Git repositories by running the 21 | # following via commandline, replacing "path/to/your/" with the actual path to your newly created 22 | # .gitignore_global file: 23 | # git config --global core.excludesfile path/to/your/.gitignore_global 24 | 25 | # CircuitPython-specific files 26 | *.mpy 27 | 28 | # Python-specific files 29 | __pycache__ 30 | *.pyc 31 | 32 | # Sphinx build-specific files 33 | _build 34 | 35 | # This file results from running `pip -e install .` in a local repository 36 | *.egg-info 37 | 38 | # Virtual environment-specific files 39 | .env 40 | .venv 41 | 42 | # MacOS-specific files 43 | *.DS_Store 44 | 45 | # IDE-specific files 46 | .idea 47 | .vscode 48 | *~ 49 | -------------------------------------------------------------------------------- /adafruit_midi/system_exclusive.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 Kevin J. Walters for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | """ 6 | `adafruit_midi.system_exclusive` 7 | ================================================================================ 8 | 9 | System Exclusive MIDI message. 10 | 11 | 12 | * Author(s): Kevin J. Walters 13 | 14 | Implementation Notes 15 | -------------------- 16 | 17 | """ 18 | 19 | from .midi_message import MIDIMessage 20 | 21 | __version__ = "0.0.0+auto.0" 22 | __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MIDI.git" 23 | 24 | 25 | class SystemExclusive(MIDIMessage): 26 | """System Exclusive MIDI message. 27 | 28 | :param list manufacturer_id: The single byte or three byte 29 | manufacturer's id as a list or bytearray of numbers between 0-127. 30 | :param list data: The 7bit data as a list or bytearray of numbers between 0-127. 31 | 32 | This message can only be parsed if it fits within the input buffer in :class:MIDI. 33 | """ 34 | 35 | _message_slots = ["manufacturer_id", "data"] 36 | _STATUS = 0xF0 37 | _STATUSMASK = 0xFF 38 | LENGTH = -1 39 | ENDSTATUS = 0xF7 40 | 41 | def __init__(self, manufacturer_id, data): 42 | self.manufacturer_id = bytes(manufacturer_id) 43 | self.data = bytes(data) 44 | super().__init__() 45 | 46 | def __bytes__(self): 47 | return bytes([self._STATUS]) + self.manufacturer_id + self.data + bytes([self.ENDSTATUS]) 48 | 49 | @classmethod 50 | def from_bytes(cls, msg_bytes): 51 | # -1 on second arg is to avoid the ENDSTATUS which is passed 52 | if msg_bytes[1] != 0: 53 | return cls(msg_bytes[1:2], msg_bytes[2:-1]) 54 | else: 55 | return cls(msg_bytes[1:4], msg_bytes[4:-1]) 56 | 57 | 58 | SystemExclusive.register_message_type() 59 | -------------------------------------------------------------------------------- /adafruit_midi/note_on.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 Kevin J. Walters for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | """ 6 | `adafruit_midi.note_on` 7 | ================================================================================ 8 | 9 | Note On Change MIDI message. 10 | 11 | 12 | * Author(s): Kevin J. Walters 13 | 14 | Implementation Notes 15 | -------------------- 16 | 17 | """ 18 | 19 | from .midi_message import MIDIMessage, note_parser 20 | 21 | __version__ = "0.0.0+auto.0" 22 | __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MIDI.git" 23 | 24 | 25 | class NoteOn(MIDIMessage): 26 | """Note On Change MIDI message. 27 | 28 | :param note: The note (key) number either as an ``int`` (0-127) or a 29 | ``str`` which is parsed, e.g. "C4" (middle C) is 60, "A4" is 69. 30 | :param int velocity: The strike velocity, 0-127, 0 is equivalent 31 | to a Note Off, defaults to 127. 32 | """ 33 | 34 | _message_slots = ["note", "velocity", "channel"] 35 | 36 | _STATUS = 0x90 37 | _STATUSMASK = 0xF0 38 | LENGTH = 3 39 | 40 | def __init__(self, note, velocity=127, *, channel=None): 41 | self.note = note_parser(note) 42 | """Key, either int (0-127) or string that will be turned on """ 43 | self.velocity = velocity 44 | """Strike velocity, int (0-127); 0 is equivalent to Note Off """ 45 | super().__init__(channel=channel) 46 | if not 0 <= self.note <= 127 or not 0 <= self.velocity <= 127: 47 | self._raise_valueerror_oor() 48 | 49 | def __bytes__(self): 50 | return bytes([self._STATUS | (self.channel & self.CHANNELMASK), self.note, self.velocity]) 51 | 52 | @classmethod 53 | def from_bytes(cls, msg_bytes): 54 | return cls(msg_bytes[1], msg_bytes[2], channel=msg_bytes[0] & cls.CHANNELMASK) 55 | 56 | 57 | NoteOn.register_message_type() 58 | -------------------------------------------------------------------------------- /examples/midi_memorycheck.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries 2 | # SPDX-License-Identifier: MIT 3 | 4 | # Check memory usage 5 | 6 | 7 | # The disable for no-member should not really be required 8 | # probably a difference between Python 3 module and micropython 9 | # 10 | # E: 8,21: Module 'gc' has no 'mem_free' member (no-member) 11 | 12 | import gc 13 | import random 14 | import time 15 | 16 | gc.collect() 17 | print(gc.mem_free()) 18 | import usb_midi 19 | 20 | gc.collect() 21 | print(gc.mem_free()) 22 | import adafruit_midi 23 | 24 | gc.collect() 25 | print(gc.mem_free()) 26 | 27 | # Full monty 28 | from adafruit_midi.channel_pressure import ChannelPressure 29 | 30 | gc.collect() 31 | print(gc.mem_free()) 32 | from adafruit_midi.control_change import ControlChange 33 | 34 | gc.collect() 35 | print(gc.mem_free()) 36 | from adafruit_midi.note_off import NoteOff 37 | 38 | gc.collect() 39 | print(gc.mem_free()) 40 | from adafruit_midi.note_on import NoteOn 41 | 42 | gc.collect() 43 | print(gc.mem_free()) 44 | from adafruit_midi.pitch_bend import PitchBend 45 | 46 | gc.collect() 47 | print(gc.mem_free()) 48 | from adafruit_midi.polyphonic_key_pressure import PolyphonicKeyPressure 49 | 50 | gc.collect() 51 | print(gc.mem_free()) 52 | from adafruit_midi.program_change import ProgramChange 53 | 54 | gc.collect() 55 | print(gc.mem_free()) 56 | from adafruit_midi.start import Start 57 | 58 | gc.collect() 59 | print(gc.mem_free()) 60 | from adafruit_midi.stop import Stop 61 | 62 | gc.collect() 63 | print(gc.mem_free()) 64 | from adafruit_midi.system_exclusive import SystemExclusive 65 | 66 | gc.collect() 67 | print(gc.mem_free()) 68 | from adafruit_midi.timing_clock import TimingClock 69 | 70 | gc.collect() 71 | print(gc.mem_free()) 72 | 73 | midi = adafruit_midi.MIDI( 74 | midi_in=usb_midi.ports[0], midi_out=usb_midi.ports[1], in_channel=0, out_channel=0 75 | ) 76 | 77 | gc.collect() 78 | print(gc.mem_free()) 79 | -------------------------------------------------------------------------------- /adafruit_midi/note_off.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 Kevin J. Walters for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | """ 6 | `adafruit_midi.note_off` 7 | ================================================================================ 8 | 9 | Note Off Change MIDI message. 10 | 11 | 12 | * Author(s): Kevin J. Walters 13 | 14 | Implementation Notes 15 | -------------------- 16 | 17 | """ 18 | 19 | from .midi_message import MIDIMessage, note_parser 20 | 21 | __version__ = "0.0.0+auto.0" 22 | __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MIDI.git" 23 | 24 | 25 | class NoteOff(MIDIMessage): 26 | """Note Off Change MIDI message. 27 | 28 | :param note: The note (key) number either as an ``int`` (0-127) or a 29 | ``str`` which is parsed, e.g. "C4" (middle C) is 60, "A4" is 69. 30 | :param int velocity: The release velocity, 0-127, defaults to 0. 31 | 32 | """ 33 | 34 | _message_slots = ["note", "velocity", "channel"] 35 | _STATUS = 0x80 36 | _STATUSMASK = 0xF0 37 | LENGTH = 3 38 | 39 | def __init__(self, note, velocity=0, *, channel=None): 40 | self.note = note_parser(note) 41 | """Key, either int (0-127) or string that will be turned off """ 42 | self.velocity = velocity 43 | """Release velocity, int (0-127) """ 44 | super().__init__(channel=channel) 45 | if not 0 <= self.note <= 127 or not 0 <= self.velocity <= 127: 46 | self._raise_valueerror_oor() 47 | 48 | def __bytes__(self): 49 | return bytes( 50 | [ 51 | self._STATUS | (self.channel & self.CHANNELMASK), 52 | self.note, 53 | self.velocity, 54 | ] 55 | ) 56 | 57 | @classmethod 58 | def from_bytes(cls, msg_bytes): 59 | return cls(msg_bytes[1], msg_bytes[2], channel=msg_bytes[0] & cls.CHANNELMASK) 60 | 61 | 62 | NoteOff.register_message_type() 63 | -------------------------------------------------------------------------------- /adafruit_midi/mtc_quarter_frame.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Raphaël Doursenaud 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | """ 6 | `adafruit_midi.mtc_quarter_frame` 7 | ================================================================================ 8 | 9 | MIDI Time Code (MTC) Quarter Frame message. 10 | 11 | 12 | * Author(s): Raphaël Doursenaud 13 | 14 | Implementation Notes 15 | -------------------- 16 | 17 | Based upon the official MMA0001 / RP004 / RP008 v4.2.1 MIDI Time Code Specification 18 | 19 | """ 20 | 21 | from adafruit_midi.midi_message import MIDIMessage 22 | 23 | __version__ = "0.0.0+auto.0" 24 | __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MIDI.git" 25 | 26 | 27 | class MtcQuarterFrame(MIDIMessage): 28 | """MIDI Time Code (MTC) Quarter Frame message. 29 | 30 | :param msgtype: The quarter frame message type: 31 | 32 | 0. Frame count LS nibble 33 | 1. Frame count MS nibble 34 | 2. Seconds count LS nibble 35 | 3. Seconds count MS nibble 36 | 4. Minutes count LS nibble 37 | 5. Minutes count MS nibble 38 | 6. Hours count LS nibble 39 | 7. Hours count MS nibble and SMPTE Type 40 | 41 | :param value: The quarter frame value for the specified type. 42 | """ 43 | 44 | _message_slots = ["msgtype", "value"] 45 | 46 | _STATUS = 0xF1 47 | _STATUSMASK = 0xFF 48 | LENGTH = 2 49 | 50 | def __init__(self, msgtype, value): 51 | self.type = msgtype 52 | self.value = value 53 | super().__init__() 54 | if not 0 <= self.type <= 7 or not 0 <= self.value <= 0x0F: 55 | self._raise_valueerror_oor() 56 | 57 | def __bytes__(self): 58 | return bytes( 59 | [ 60 | self._STATUS, 61 | (self.type << 4) + self.value, # Assemble low and high nibbles 62 | ] 63 | ) 64 | 65 | @classmethod 66 | def from_bytes(cls, msg_bytes): 67 | return cls(msg_bytes[1] >> 4, msg_bytes[1] & 15) # High nibble # Low nibble 68 | 69 | 70 | MtcQuarterFrame.register_message_type() 71 | -------------------------------------------------------------------------------- /adafruit_midi/control_change_values.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Mark Komus for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | # The MIT License (MIT) 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | # THE SOFTWARE. 24 | """ 25 | `adafruit_midi.control_change_values` 26 | ================================================================================ 27 | 28 | Definition for standard MIDI control change values. 29 | 30 | 31 | * Author(s): Mark Komus 32 | 33 | Implementation Notes 34 | -------------------- 35 | 36 | """ 37 | 38 | __version__ = "0.0.0+auto.0" 39 | __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MIDI.git" 40 | 41 | MOD_WHEEL = 1 42 | BREATH_CONTROL = 2 43 | FOOT_CONTROLLER = 4 44 | VOLUME = 7 45 | PAN = 10 46 | EXPRESSION = 11 47 | PORTAMENTO_TIME = 5 48 | SUSTAIN_PEDAL = 64 49 | PORTAMENTO = 65 50 | FILTER_RESONANCE = 71 51 | RELEASE_TIME = 72 52 | ATTACK_TIME = 73 53 | CUTOFF_FREQUENCY = 74 54 | DECAY_TIME = 75 55 | VIBRATO_RATE = 76 56 | VIBRATO_DEPTH = 77 57 | VIBRATO_DELAY = 78 58 | CHORUS = 93 59 | ALL_CONTROLLERS_OFF = 121 60 | ALL_NOTES_OFF = 123 61 | -------------------------------------------------------------------------------- /tests/test_note_parser.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 Kevin J. Walters for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | import os 6 | import unittest 7 | 8 | verbose = int(os.getenv("TESTVERBOSE", "2")) 9 | 10 | # adafruit_midi had an import usb_midi 11 | import sys 12 | 13 | # sys.modules['usb_midi'] = MagicMock() 14 | 15 | # Borrowing the dhalbert/tannewt technique from adafruit/Adafruit_CircuitPython_Motor 16 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) 17 | 18 | from adafruit_midi.midi_message import note_parser 19 | 20 | 21 | class Test_note_parser(unittest.TestCase): 22 | def text_int_passthru(self): 23 | self.assertEqual(note_parser(0), 0) 24 | self.assertEqual(note_parser(70), 70) 25 | self.assertEqual(note_parser(127), 127) 26 | 27 | # it does not range check so these should pass 28 | self.assertEqual(note_parser(-303), -303) 29 | self.assertEqual(note_parser(808), 808) 30 | 31 | def test_good_text(self): 32 | note_prefix = { 33 | "Cb": 11, 34 | "C": 12, 35 | "C#": 13, 36 | "Db": 13, 37 | "D": 14, 38 | "D#": 15, 39 | "Eb": 15, 40 | "E": 16, 41 | "Fb": 16, 42 | "E#": 17, 43 | "F": 17, 44 | "F#": 18, 45 | "Gb": 18, 46 | "G": 19, 47 | "G#": 20, 48 | "Ab": 20, 49 | "A": 21, 50 | "A#": 22, 51 | "Bb": 22, 52 | "B": 23, 53 | "B#": 24, 54 | } 55 | 56 | # test from Cb0 to B#8 57 | for prefix, base_value in note_prefix.items(): 58 | for octave in range(9): 59 | note = prefix + str(octave) 60 | expected_value = base_value + octave * 12 # 12 semitones in octave 61 | self.assertEqual(note_parser(note), expected_value) 62 | 63 | # re-test with simple C4/A4 tests to catch any bugs in above 64 | self.assertEqual(note_parser("C4"), 60) 65 | self.assertEqual(note_parser("A4"), 69) 66 | 67 | def test_bad_text(self): 68 | for text_note in ["H", "H4", "asdfasdfasdf", "000", "999"]: 69 | with self.assertRaises(ValueError): 70 | note_parser(text_note) 71 | 72 | 73 | if __name__ == "__main__": 74 | unittest.main(verbosity=verbose) 75 | -------------------------------------------------------------------------------- /examples/midi_inoutdemo.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries 2 | # SPDX-License-Identifier: MIT 3 | 4 | # midi_inoutdemo - demonstrates receiving and sending MIDI events 5 | 6 | import usb_midi 7 | 8 | import adafruit_midi 9 | from adafruit_midi.channel_pressure import ChannelPressure 10 | from adafruit_midi.control_change import ControlChange 11 | from adafruit_midi.midi_message import MIDIUnknownEvent 12 | from adafruit_midi.note_off import NoteOff 13 | from adafruit_midi.note_on import NoteOn 14 | from adafruit_midi.pitch_bend import PitchBend 15 | from adafruit_midi.polyphonic_key_pressure import PolyphonicKeyPressure 16 | from adafruit_midi.program_change import ProgramChange 17 | from adafruit_midi.start import Start 18 | from adafruit_midi.stop import Stop 19 | from adafruit_midi.system_exclusive import SystemExclusive 20 | from adafruit_midi.timing_clock import TimingClock 21 | 22 | # TimingClock is worth importing first if present as it 23 | # will make parsing more efficient for this high frequency event 24 | # Only importing what is used will save a little bit of memory 25 | 26 | 27 | midi = adafruit_midi.MIDI( 28 | midi_in=usb_midi.ports[0], 29 | midi_out=usb_midi.ports[1], 30 | in_channel=(1, 2, 3), 31 | out_channel=0, 32 | ) 33 | 34 | print("Midi Demo in and out") 35 | 36 | # Convert channel numbers at the presentation layer to the ones musicians use 37 | print("Default output channel:", midi.out_channel + 1) 38 | print("Listening on input channels:", tuple(c + 1 for c in midi.in_channel)) 39 | 40 | major_chord = [0, 4, 7] 41 | while True: 42 | while True: 43 | msg_in = midi.receive() # non-blocking read 44 | # For a Note On or Note Off play a major chord 45 | # For any other known event just forward it 46 | if isinstance(msg_in, NoteOn) and msg_in.velocity != 0: 47 | print( 48 | "Playing major chord with root", 49 | msg_in.note, 50 | "from channel", 51 | msg_in.channel + 1, 52 | ) 53 | for offset in major_chord: 54 | new_note = msg_in.note + offset 55 | if 0 <= new_note <= 127: 56 | midi.send(NoteOn(new_note, msg_in.velocity)) 57 | 58 | elif isinstance(msg_in, NoteOff) or isinstance(msg_in, NoteOn) and msg_in.velocity == 0: 59 | for offset in major_chord: 60 | new_note = msg_in.note + offset 61 | if 0 <= new_note <= 127: 62 | midi.send(NoteOff(new_note, 0x00)) 63 | 64 | elif isinstance(msg_in, MIDIUnknownEvent): 65 | # Message are only known if they are imported 66 | print("Unknown MIDI event status ", msg_in.status) 67 | 68 | elif msg_in is not None: 69 | midi.send(msg_in) 70 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | .. image:: https://readthedocs.org/projects/adafruit-circuitpython-midi/badge/?version=latest 5 | :target: https://docs.circuitpython.org/projects/midi/en/latest/ 6 | :alt: Documentation Status 7 | 8 | .. image:: https://raw.githubusercontent.com/adafruit/Adafruit_CircuitPython_Bundle/main/badges/adafruit_discord.svg 9 | :target: https://adafru.it/discord 10 | :alt: Discord 11 | 12 | .. image:: https://github.com/adafruit/Adafruit_CircuitPython_MIDI/workflows/Build%20CI/badge.svg 13 | :target: https://github.com/adafruit/Adafruit_CircuitPython_MIDI/actions/ 14 | :alt: Build Status 15 | 16 | .. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json 17 | :target: https://github.com/astral-sh/ruff 18 | :alt: Code Style: Ruff 19 | 20 | A CircuitPython helper for encoding/decoding MIDI packets over a MIDI or UART connection. 21 | 22 | 23 | Dependencies 24 | ============= 25 | This driver depends on: 26 | 27 | * `Adafruit CircuitPython `_ 28 | 29 | Please ensure all dependencies are available on the CircuitPython filesystem. 30 | This is easily achieved by downloading 31 | `the Adafruit library and driver bundle `_. 32 | 33 | Installing from PyPI 34 | -------------------- 35 | 36 | On supported GNU/Linux systems like the Raspberry Pi, you can install the driver locally `from 37 | PyPI `_. To install for current user: 38 | 39 | .. code-block:: shell 40 | 41 | pip3 install adafruit-circuitpython-midi 42 | 43 | To install system-wide (this may be required in some cases): 44 | 45 | .. code-block:: shell 46 | 47 | sudo pip3 install adafruit-circuitpython-midi 48 | 49 | To install in a virtual environment in your current project: 50 | 51 | .. code-block:: shell 52 | 53 | mkdir project-name && cd project-name 54 | python3 -m venv .venv 55 | source .venv/bin/activate 56 | pip3 install adafruit-circuitpython-midi 57 | 58 | Usage Example 59 | ============= 60 | 61 | .. code-block:: python 62 | 63 | import time 64 | import random 65 | import usb_midi 66 | import adafruit_midi 67 | 68 | from adafruit_midi.note_on import NoteOn 69 | from adafruit_midi.note_off import NoteOff 70 | from adafruit_midi.pitch_bend import PitchBend 71 | from adafruit_midi.control_change import ControlChange 72 | 73 | midi = adafruit_midi.MIDI(midi_out=usb_midi.ports[1], out_channel=0) 74 | 75 | print("Midi test") 76 | 77 | print("Default output MIDI channel:", midi.out_channel + 1) 78 | 79 | while True: 80 | midi.send(NoteOn(44, 120)) # G sharp 2nd octave 81 | time.sleep(0.25) 82 | a_pitch_bend = PitchBend(random.randint(0, 16383)) 83 | midi.send(a_pitch_bend) 84 | time.sleep(0.25) 85 | midi.send([NoteOff("G#2", 120), 86 | ControlChange(3, 44)]) 87 | time.sleep(0.5) 88 | 89 | 90 | Documentation 91 | ============= 92 | 93 | API documentation for this library can be found on `Read the Docs `_. 94 | 95 | For information on building library documentation, please check out `this guide `_. 96 | 97 | Contributing 98 | ============ 99 | 100 | Contributions are welcome! Please read our `Code of Conduct 101 | `_ 102 | before contributing to help this project stay welcoming. 103 | -------------------------------------------------------------------------------- /ruff.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Tim Cocks for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | target-version = "py38" 6 | line-length = 100 7 | 8 | [lint] 9 | preview = true 10 | select = ["I", "PL", "UP"] 11 | 12 | extend-select = [ 13 | "D419", # empty-docstring 14 | "E501", # line-too-long 15 | "W291", # trailing-whitespace 16 | "PLC0414", # useless-import-alias 17 | "PLC2401", # non-ascii-name 18 | "PLC2801", # unnecessary-dunder-call 19 | "PLC3002", # unnecessary-direct-lambda-call 20 | "PLE0101", # return-in-init 21 | "F706", # return-outside-function 22 | "F704", # yield-outside-function 23 | "PLE0116", # continue-in-finally 24 | "PLE0117", # nonlocal-without-binding 25 | "PLE0241", # duplicate-bases 26 | "PLE0302", # unexpected-special-method-signature 27 | "PLE0604", # invalid-all-object 28 | "PLE0605", # invalid-all-format 29 | "PLE0643", # potential-index-error 30 | "PLE0704", # misplaced-bare-raise 31 | "PLE1141", # dict-iter-missing-items 32 | "PLE1142", # await-outside-async 33 | "PLE1205", # logging-too-many-args 34 | "PLE1206", # logging-too-few-args 35 | "PLE1307", # bad-string-format-type 36 | "PLE1310", # bad-str-strip-call 37 | "PLE1507", # invalid-envvar-value 38 | "PLE2502", # bidirectional-unicode 39 | "PLE2510", # invalid-character-backspace 40 | "PLE2512", # invalid-character-sub 41 | "PLE2513", # invalid-character-esc 42 | "PLE2514", # invalid-character-nul 43 | "PLE2515", # invalid-character-zero-width-space 44 | "PLR0124", # comparison-with-itself 45 | "PLR0202", # no-classmethod-decorator 46 | "PLR0203", # no-staticmethod-decorator 47 | "UP004", # useless-object-inheritance 48 | "PLR0206", # property-with-parameters 49 | "PLR0904", # too-many-public-methods 50 | "PLR0911", # too-many-return-statements 51 | "PLR0912", # too-many-branches 52 | "PLR0913", # too-many-arguments 53 | "PLR0914", # too-many-locals 54 | "PLR0915", # too-many-statements 55 | "PLR0916", # too-many-boolean-expressions 56 | "PLR1702", # too-many-nested-blocks 57 | "PLR1704", # redefined-argument-from-local 58 | "PLR1711", # useless-return 59 | "C416", # unnecessary-comprehension 60 | "PLR1733", # unnecessary-dict-index-lookup 61 | "PLR1736", # unnecessary-list-index-lookup 62 | 63 | # ruff reports this rule is unstable 64 | #"PLR6301", # no-self-use 65 | 66 | "PLW0108", # unnecessary-lambda 67 | "PLW0120", # useless-else-on-loop 68 | "PLW0127", # self-assigning-variable 69 | "PLW0129", # assert-on-string-literal 70 | "B033", # duplicate-value 71 | "PLW0131", # named-expr-without-context 72 | "PLW0245", # super-without-brackets 73 | "PLW0406", # import-self 74 | "PLW0602", # global-variable-not-assigned 75 | "PLW0603", # global-statement 76 | "PLW0604", # global-at-module-level 77 | 78 | # fails on the try: import typing used by libraries 79 | #"F401", # unused-import 80 | 81 | "F841", # unused-variable 82 | "E722", # bare-except 83 | "PLW0711", # binary-op-exception 84 | "PLW1501", # bad-open-mode 85 | "PLW1508", # invalid-envvar-default 86 | "PLW1509", # subprocess-popen-preexec-fn 87 | "PLW2101", # useless-with-lock 88 | "PLW3301", # nested-min-max 89 | ] 90 | 91 | ignore = [ 92 | "PLR2004", # magic-value-comparison 93 | "UP030", # format literals 94 | "PLW1514", # unspecified-encoding 95 | "PLR0913", # too-many-arguments 96 | "PLR0915", # too-many-statements 97 | "PLR0917", # too-many-positional-arguments 98 | "PLR0904", # too-many-public-methods 99 | "PLR0912", # too-many-branches 100 | "PLR0916", # too-many-boolean-expressions 101 | "PLR6301", # could-be-static no-self-use 102 | "PLC0415", # import outside toplevel 103 | "PLC2701", # private import 104 | "PLC2801", # dunder bytes call 105 | ] 106 | 107 | [format] 108 | line-ending = "lf" 109 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | import datetime 6 | import os 7 | import sys 8 | 9 | sys.path.insert(0, os.path.abspath("..")) 10 | 11 | # -- General configuration ------------------------------------------------ 12 | 13 | # Add any Sphinx extension module names here, as strings. They can be 14 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 15 | # ones. 16 | extensions = [ 17 | "sphinx.ext.autodoc", 18 | "sphinxcontrib.jquery", 19 | "sphinx.ext.intersphinx", 20 | "sphinx.ext.napoleon", 21 | "sphinx.ext.todo", 22 | ] 23 | 24 | # TODO: Please Read! 25 | # Uncomment the below if you use native CircuitPython modules such as 26 | # digitalio, micropython and busio. List the modules you use. Without it, the 27 | # autodoc module docs will fail to generate with a warning. 28 | autodoc_mock_imports = [] 29 | 30 | 31 | intersphinx_mapping = { 32 | "python": ("https://docs.python.org/3", None), 33 | "CircuitPython": ("https://docs.circuitpython.org/en/latest/", None), 34 | } 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ["_templates"] 38 | 39 | source_suffix = ".rst" 40 | 41 | # The master toctree document. 42 | master_doc = "index" 43 | 44 | # General information about the project. 45 | project = "Adafruit MIDI Library" 46 | creation_year = "2019" 47 | current_year = str(datetime.datetime.now().year) 48 | year_duration = ( 49 | current_year if current_year == creation_year else creation_year + " - " + current_year 50 | ) 51 | copyright = year_duration + " Ladyada & Kevin J. Walters" 52 | author = "Ladyada & Kevin J. Walters" 53 | 54 | # The version info for the project you're documenting, acts as replacement for 55 | # |version| and |release|, also used in various other places throughout the 56 | # built documents. 57 | # 58 | # The short X.Y version. 59 | version = "1.0" 60 | # The full version, including alpha/beta/rc tags. 61 | release = "1.0" 62 | 63 | # The language for content autogenerated by Sphinx. Refer to documentation 64 | # for a list of supported languages. 65 | # 66 | # This is also used if you do content translation via gettext catalogs. 67 | # Usually you set "language" from the command line for these cases. 68 | language = "en" 69 | 70 | # List of patterns, relative to source directory, that match files and 71 | # directories to ignore when looking for source files. 72 | # This patterns also effect to html_static_path and html_extra_path 73 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", ".env", "CODE_OF_CONDUCT.md"] 74 | 75 | # The reST default role (used for this markup: `text`) to use for all 76 | # documents. 77 | # 78 | default_role = "any" 79 | 80 | # If true, '()' will be appended to :func: etc. cross-reference text. 81 | # 82 | add_function_parentheses = True 83 | 84 | # The name of the Pygments (syntax highlighting) style to use. 85 | pygments_style = "sphinx" 86 | 87 | # If true, `todo` and `todoList` produce output, else they produce nothing. 88 | todo_include_todos = False 89 | 90 | # If this is True, todo emits a warning for each TODO entries. The default is False. 91 | todo_emit_warnings = True 92 | 93 | napoleon_numpy_docstring = False 94 | 95 | # -- Options for HTML output ---------------------------------------------- 96 | 97 | # The theme to use for HTML and HTML Help pages. See the documentation for 98 | # a list of builtin themes. 99 | # 100 | import sphinx_rtd_theme 101 | 102 | html_theme = "sphinx_rtd_theme" 103 | 104 | # Add any paths that contain custom static files (such as style sheets) here, 105 | # relative to this directory. They are copied after the builtin static files, 106 | # so a file named "default.css" will overwrite the builtin "default.css". 107 | html_static_path = ["_static"] 108 | 109 | # Include extra css to work around rtd theme glitches 110 | html_css_files = ["custom.css"] 111 | 112 | # The name of an image file (relative to this directory) to use as a favicon of 113 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 114 | # pixels large. 115 | # 116 | html_favicon = "_static/favicon.ico" 117 | 118 | # Output file base name for HTML help builder. 119 | htmlhelp_basename = "AdafruitMidiLibrarydoc" 120 | 121 | # -- Options for LaTeX output --------------------------------------------- 122 | 123 | latex_elements = { 124 | # The paper size ('letterpaper' or 'a4paper'). 125 | # 126 | # 'papersize': 'letterpaper', 127 | # The font size ('10pt', '11pt' or '12pt'). 128 | # 129 | # 'pointsize': '10pt', 130 | # Additional stuff for the LaTeX preamble. 131 | # 132 | # 'preamble': '', 133 | # Latex figure (float) alignment 134 | # 135 | # 'figure_align': 'htbp', 136 | } 137 | 138 | # Grouping the document tree into LaTeX files. List of tuples 139 | # (source start file, target name, title, 140 | # author, documentclass [howto, manual, or own class]). 141 | latex_documents = [ 142 | ( 143 | master_doc, 144 | "AdafruitMIDILibrary.tex", 145 | "AdafruitMIDI Library Documentation", 146 | author, 147 | "manual", 148 | ), 149 | ] 150 | 151 | # -- Options for manual page output --------------------------------------- 152 | 153 | # One entry per manual page. List of tuples 154 | # (source start file, name, description, authors, manual section). 155 | man_pages = [ 156 | ( 157 | master_doc, 158 | "AdafruitMIDIlibrary", 159 | "Adafruit MIDI Library Documentation", 160 | [author], 161 | 1, 162 | ) 163 | ] 164 | 165 | # -- Options for Texinfo output ------------------------------------------- 166 | 167 | # Grouping the document tree into Texinfo files. List of tuples 168 | # (source start file, target name, title, author, 169 | # dir menu entry, description, category) 170 | texinfo_documents = [ 171 | ( 172 | master_doc, 173 | "AdafruitMIDILibrary", 174 | "Adafruit MIDI Library Documentation", 175 | author, 176 | "AdafruitMIDILibrary", 177 | "One line description of project.", 178 | "Miscellaneous", 179 | ), 180 | ] 181 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # Adafruit Community Code of Conduct 8 | 9 | ## Our Pledge 10 | 11 | In the interest of fostering an open and welcoming environment, we as 12 | contributors and leaders pledge to making participation in our project and 13 | our community a harassment-free experience for everyone, regardless of age, body 14 | size, disability, ethnicity, gender identity and expression, level or type of 15 | experience, education, socio-economic status, nationality, personal appearance, 16 | race, religion, or sexual identity and orientation. 17 | 18 | ## Our Standards 19 | 20 | We are committed to providing a friendly, safe and welcoming environment for 21 | all. 22 | 23 | Examples of behavior that contributes to creating a positive environment 24 | include: 25 | 26 | * Be kind and courteous to others 27 | * Using welcoming and inclusive language 28 | * Being respectful of differing viewpoints and experiences 29 | * Collaborating with other community members 30 | * Gracefully accepting constructive criticism 31 | * Focusing on what is best for the community 32 | * Showing empathy towards other community members 33 | 34 | Examples of unacceptable behavior by participants include: 35 | 36 | * The use of sexualized language or imagery and sexual attention or advances 37 | * The use of inappropriate images, including in a community member's avatar 38 | * The use of inappropriate language, including in a community member's nickname 39 | * Any spamming, flaming, baiting or other attention-stealing behavior 40 | * Excessive or unwelcome helping; answering outside the scope of the question 41 | asked 42 | * Trolling, insulting/derogatory comments, and personal or political attacks 43 | * Promoting or spreading disinformation, lies, or conspiracy theories against 44 | a person, group, organisation, project, or community 45 | * Public or private harassment 46 | * Publishing others' private information, such as a physical or electronic 47 | address, without explicit permission 48 | * Other conduct which could reasonably be considered inappropriate 49 | 50 | The goal of the standards and moderation guidelines outlined here is to build 51 | and maintain a respectful community. We ask that you don’t just aim to be 52 | "technically unimpeachable", but rather try to be your best self. 53 | 54 | We value many things beyond technical expertise, including collaboration and 55 | supporting others within our community. Providing a positive experience for 56 | other community members can have a much more significant impact than simply 57 | providing the correct answer. 58 | 59 | ## Our Responsibilities 60 | 61 | Project leaders are responsible for clarifying the standards of acceptable 62 | behavior and are expected to take appropriate and fair corrective action in 63 | response to any instances of unacceptable behavior. 64 | 65 | Project leaders have the right and responsibility to remove, edit, or 66 | reject messages, comments, commits, code, issues, and other contributions 67 | that are not aligned to this Code of Conduct, or to ban temporarily or 68 | permanently any community member for other behaviors that they deem 69 | inappropriate, threatening, offensive, or harmful. 70 | 71 | ## Moderation 72 | 73 | Instances of behaviors that violate the Adafruit Community Code of Conduct 74 | may be reported by any member of the community. Community members are 75 | encouraged to report these situations, including situations they witness 76 | involving other community members. 77 | 78 | You may report in the following ways: 79 | 80 | In any situation, you may send an email to . 81 | 82 | On the Adafruit Discord, you may send an open message from any channel 83 | to all Community Moderators by tagging @community moderators. You may 84 | also send an open message from any channel, or a direct message to 85 | @kattni#1507, @tannewt#4653, @Dan Halbert#1614, @cater#2442, 86 | @sommersoft#0222, @Mr. Certainly#0472 or @Andon#8175. 87 | 88 | Email and direct message reports will be kept confidential. 89 | 90 | In situations on Discord where the issue is particularly egregious, possibly 91 | illegal, requires immediate action, or violates the Discord terms of service, 92 | you should also report the message directly to Discord. 93 | 94 | These are the steps for upholding our community’s standards of conduct. 95 | 96 | 1. Any member of the community may report any situation that violates the 97 | Adafruit Community Code of Conduct. All reports will be reviewed and 98 | investigated. 99 | 2. If the behavior is an egregious violation, the community member who 100 | committed the violation may be banned immediately, without warning. 101 | 3. Otherwise, moderators will first respond to such behavior with a warning. 102 | 4. Moderators follow a soft "three strikes" policy - the community member may 103 | be given another chance, if they are receptive to the warning and change their 104 | behavior. 105 | 5. If the community member is unreceptive or unreasonable when warned by a 106 | moderator, or the warning goes unheeded, they may be banned for a first or 107 | second offense. Repeated offenses will result in the community member being 108 | banned. 109 | 110 | ## Scope 111 | 112 | This Code of Conduct and the enforcement policies listed above apply to all 113 | Adafruit Community venues. This includes but is not limited to any community 114 | spaces (both public and private), the entire Adafruit Discord server, and 115 | Adafruit GitHub repositories. Examples of Adafruit Community spaces include 116 | but are not limited to meet-ups, audio chats on the Adafruit Discord, or 117 | interaction at a conference. 118 | 119 | This Code of Conduct applies both within project spaces and in public spaces 120 | when an individual is representing the project or its community. As a community 121 | member, you are representing our community, and are expected to behave 122 | accordingly. 123 | 124 | ## Attribution 125 | 126 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 127 | version 1.4, available at 128 | , 129 | and the [Rust Code of Conduct](https://www.rust-lang.org/en-US/conduct.html). 130 | 131 | For other projects adopting the Adafruit Community Code of 132 | Conduct, please contact the maintainers of those projects for enforcement. 133 | If you wish to use this code of conduct for your own project, consider 134 | explicitly mentioning your moderation policy or making a copy with your 135 | own moderation policy so as to avoid confusion. 136 | -------------------------------------------------------------------------------- /adafruit_midi/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 Limor Fried for Adafruit Industries 2 | # SPDX-FileCopyrightText: 2019 Kevin J. Walters for Adafruit Industries 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | """ 7 | `adafruit_midi` 8 | ================================================================================ 9 | 10 | A CircuitPython helper for encoding/decoding MIDI packets over a MIDI or UART connection. 11 | 12 | 13 | * Author(s): Limor Fried, Kevin J. Walters 14 | 15 | Implementation Notes 16 | -------------------- 17 | 18 | **Hardware:** 19 | 20 | 21 | 22 | **Software and Dependencies:** 23 | 24 | * Adafruit CircuitPython firmware for the supported boards: 25 | https://github.com/adafruit/circuitpython/releases 26 | 27 | """ 28 | 29 | try: 30 | from typing import Any, BinaryIO, Dict, List, Optional, Tuple, Union 31 | except ImportError: 32 | pass 33 | 34 | from .midi_message import MIDIMessage 35 | 36 | __version__ = "0.0.0+auto.0" 37 | __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MIDI.git" 38 | 39 | 40 | class MIDI: 41 | """MIDI helper class. ``midi_in`` or ``midi_out`` *must* be set or both together. 42 | 43 | :param midi_in: an object which implements ``read(length)``, 44 | set to ``usb_midi.ports[0]`` for USB MIDI, default None. 45 | :param midi_out: an object which implements ``write(buffer, length)``, 46 | set to ``usb_midi.ports[1]`` for USB MIDI, default None. 47 | :param in_channel: The input channel(s). 48 | This is used by ``receive`` to filter data. 49 | This can either be an ``int`` for the wire protocol channel number (0-15) 50 | a tuple of ``int`` to listen for multiple channels. 51 | Defaults to all channels. 52 | :param int out_channel: The wire protocol output channel number (0-15) 53 | used by ``send`` if no channel is specified, 54 | defaults to 0 (MIDI Channel 1). 55 | :param int in_buf_size: Maximum size of input buffer in bytes, default 30. 56 | :param bool debug: Debug mode, default False. 57 | 58 | """ 59 | 60 | def __init__( 61 | self, 62 | midi_in: Optional[BinaryIO] = None, 63 | midi_out: Optional[BinaryIO] = None, 64 | *, 65 | in_channel: Optional[Union[int, Tuple[int, ...]]] = None, 66 | out_channel: int = 0, 67 | in_buf_size: int = 30, 68 | debug: bool = False, 69 | ): 70 | if midi_in is None and midi_out is None: 71 | raise ValueError("No midi_in or midi_out provided") 72 | self._midi_in = midi_in 73 | self._midi_out = midi_out 74 | self._in_channel = in_channel 75 | self.in_channel = in_channel 76 | self._out_channel = out_channel 77 | self.out_channel = out_channel 78 | self._debug = debug 79 | # This input buffer holds what has been read from midi_in 80 | self._in_buf = bytearray(0) 81 | self._in_buf_size = in_buf_size 82 | self._outbuf = bytearray(4) 83 | self._skipped_bytes = 0 84 | 85 | @property 86 | def in_channel(self) -> Optional[Union[int, Tuple[int, ...]]]: 87 | """The incoming MIDI channel. Must be 0-15. Correlates to MIDI channels 1-16, e.g. 88 | ``in_channel = 3`` will listen on MIDI channel 4. 89 | Can also listen on multiple channels, e.g. ``in_channel = (0,1,2)`` 90 | will listen on MIDI channels 1-3. 91 | Default is all channels.""" 92 | return self._in_channel 93 | 94 | @in_channel.setter 95 | def in_channel(self, channel: Optional[Union[str, int, Tuple[int, ...]]]) -> None: 96 | if channel is None or channel == "ALL": 97 | self._in_channel = tuple(range(16)) 98 | elif isinstance(channel, int) and 0 <= channel <= 15: 99 | self._in_channel = channel 100 | elif isinstance(channel, tuple) and all(0 <= c <= 15 for c in channel): 101 | self._in_channel = channel 102 | else: 103 | raise RuntimeError("Invalid input channel") 104 | 105 | @property 106 | def out_channel(self) -> int: 107 | """The outgoing MIDI channel. Must be 0-15. Correlates to MIDI channels 1-16, e.g. 108 | ``out_channel = 3`` will send to MIDI channel 4. Default is 0 (MIDI channel 1). 109 | """ 110 | return self._out_channel 111 | 112 | @out_channel.setter 113 | def out_channel(self, channel: int) -> None: 114 | if not 0 <= channel <= 15: 115 | raise RuntimeError("Invalid output channel") 116 | self._out_channel = channel 117 | 118 | def receive(self) -> Optional[MIDIMessage]: 119 | """Read messages from MIDI port, store them in internal read buffer, then parse that data 120 | and return the first MIDI message (event). 121 | This maintains the blocking characteristics of the midi_in port. 122 | 123 | :returns MIDIMessage object: Returns object or None for nothing. 124 | """ 125 | ### could check _midi_in is an object OR correct object OR correct interface here? 126 | # If the buffer here is not full then read as much as we can fit from 127 | # the input port 128 | if len(self._in_buf) < self._in_buf_size: 129 | bytes_in = self._midi_in.read(self._in_buf_size - len(self._in_buf)) 130 | if bytes_in: 131 | if self._debug: 132 | print("Receiving: ", [hex(i) for i in bytes_in]) 133 | self._in_buf.extend(bytes_in) 134 | del bytes_in 135 | 136 | (msg, endplusone, skipped) = MIDIMessage.from_message_bytes(self._in_buf, self._in_channel) 137 | if endplusone != 0: 138 | # This is not particularly efficient as it's copying most of bytearray 139 | # and deleting old one 140 | self._in_buf = self._in_buf[endplusone:] 141 | 142 | self._skipped_bytes += skipped 143 | 144 | # msg could still be None at this point, e.g. in middle of monster SysEx 145 | return msg 146 | 147 | def send(self, msg: MIDIMessage, channel: Optional[int] = None) -> None: 148 | """Sends a MIDI message. 149 | 150 | :param msg: Either a MIDIMessage object or a sequence (list) of MIDIMessage objects. 151 | The channel property will be *updated* as a side-effect of sending message(s). 152 | :param int channel: Channel number, if not set the ``out_channel`` will be used. 153 | 154 | """ 155 | if channel is None: 156 | channel = self.out_channel 157 | if isinstance(msg, MIDIMessage): 158 | msg.channel = channel 159 | # bytes(object) does not work in uPy 160 | data = msg.__bytes__() 161 | else: 162 | data = bytearray() 163 | for each_msg in msg: 164 | each_msg.channel = channel 165 | data.extend(each_msg.__bytes__()) 166 | 167 | self._send(data, len(data)) 168 | 169 | def _send(self, packet: bytes, num: int) -> None: 170 | if self._debug: 171 | print("Sending: ", [hex(i) for i in packet[:num]]) 172 | self._midi_out.write(packet, num) 173 | -------------------------------------------------------------------------------- /adafruit_midi/midi_message.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 Kevin J. Walters for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | """ 6 | `adafruit_midi.midi_message` 7 | ================================================================================ 8 | 9 | An abstract class for objects which represent MIDI messages (events). 10 | When individual messages are imported they register themselves with 11 | :func:register_message_type which makes them recognised 12 | by the parser, :func:from_message_bytes. 13 | 14 | Large messages like :class:SystemExclusive can only be parsed if they fit 15 | within the input buffer in :class:MIDI. 16 | 17 | 18 | * Author(s): Kevin J. Walters 19 | 20 | Implementation Notes 21 | -------------------- 22 | 23 | """ 24 | 25 | __version__ = "0.0.0+auto.0" 26 | __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_MIDI.git" 27 | 28 | try: 29 | from typing import Any, List, Optional, Tuple, Union 30 | except ImportError: 31 | pass 32 | 33 | # From C3 - A and B are above G 34 | # Semitones A B C D E F G 35 | NOTE_OFFSET = [21, 23, 12, 14, 16, 17, 19] 36 | 37 | 38 | def channel_filter(channel: int, channel_spec: Optional[Union[int, Tuple[int, ...]]]) -> bool: 39 | """ 40 | Utility function to return True iff the given channel matches channel_spec. 41 | """ 42 | if isinstance(channel_spec, int): 43 | return channel == channel_spec 44 | if isinstance(channel_spec, tuple): 45 | return channel in channel_spec 46 | raise ValueError("Incorrect type for channel_spec" + str(type(channel_spec))) 47 | 48 | 49 | def note_parser(note: Union[int, str]) -> int: 50 | """If note is a string then it will be parsed and converted to a MIDI note (key) number, e.g. 51 | "C4" will return 60, "C#4" will return 61. If note is not a string it will simply be returned. 52 | 53 | :param note: Either 0-127 int or a str representing the note, e.g. "C#4" 54 | """ 55 | if isinstance(note, str): 56 | if len(note) < 2: 57 | raise ValueError("Bad note format") 58 | noteidx = ord(note[0].upper()) - 65 # 65 os ord('A') 59 | if not 0 <= noteidx <= 6: 60 | raise ValueError("Bad note") 61 | sharpen = 0 62 | if note[1] == "#": 63 | sharpen = 1 64 | elif note[1] == "b": 65 | sharpen = -1 66 | # int may throw exception here 67 | midi_note = int(note[1 + abs(sharpen) :]) * 12 + NOTE_OFFSET[noteidx] + sharpen 68 | elif isinstance(note, int): 69 | midi_note = note 70 | return midi_note 71 | 72 | 73 | class MIDIMessage: 74 | """ 75 | The parent class for MIDI messages. 76 | 77 | Class variables: 78 | 79 | * ``_STATUS`` - extracted from status byte with channel replaced by 0s 80 | (high bit is always set by convention). 81 | * ``_STATUSMASK`` - mask used to compared a status byte with ``_STATUS`` value. 82 | * ``LENGTH`` - length for a fixed size message *including* status 83 | or -1 for variable length. 84 | * ``CHANNELMASK`` - mask used to apply a (wire protocol) channel number. 85 | * ``ENDSTATUS`` - the end of message status byte, only set for variable length. 86 | 87 | This is an *abstract* class. 88 | """ 89 | 90 | _STATUS: Optional[int] = None 91 | _STATUSMASK = None 92 | LENGTH: Optional[int] = None 93 | CHANNELMASK = 0x0F 94 | ENDSTATUS = None 95 | 96 | # Commonly used exceptions to save memory 97 | @staticmethod 98 | def _raise_valueerror_oor() -> None: 99 | raise ValueError("Out of range") 100 | 101 | # Each element is ((status, mask), class) 102 | # order is more specific masks first 103 | # Add better type hints for status, mask, class referenced above 104 | _statusandmask_to_class: List[Tuple[Tuple[Optional[bytes], Optional[int]], "MIDIMessage"]] = [] 105 | 106 | def __init__(self, *, channel: Optional[int] = None) -> None: 107 | self._channel = channel # dealing with pylint inadequacy 108 | self.channel = channel 109 | 110 | @property 111 | def channel(self) -> Optional[int]: 112 | """The channel number of the MIDI message where appropriate. 113 | This is *updated* by MIDI.send() method. 114 | """ 115 | return self._channel 116 | 117 | @channel.setter 118 | def channel(self, channel: int) -> None: 119 | if channel is not None and not 0 <= channel <= 15: 120 | raise ValueError("Channel must be 0-15 or None") 121 | self._channel = channel 122 | 123 | @classmethod 124 | def register_message_type(cls) -> None: 125 | """Register a new message by its status value and mask. 126 | This is called automagically at ``import`` time for each message. 127 | """ 128 | ### These must be inserted with more specific masks first 129 | insert_idx = len(MIDIMessage._statusandmask_to_class) 130 | for idx, m_type in enumerate(MIDIMessage._statusandmask_to_class): 131 | if cls._STATUSMASK > m_type[0][1]: 132 | insert_idx = idx 133 | break 134 | 135 | MIDIMessage._statusandmask_to_class.insert( 136 | insert_idx, ((cls._STATUS, cls._STATUSMASK), cls) 137 | ) 138 | 139 | @classmethod 140 | def _search_eom_status( 141 | cls, 142 | buf: bytearray, 143 | eom_status: Optional[int], 144 | msgstartidx: int, 145 | msgendidxplusone: int, 146 | endidx: int, 147 | ) -> Tuple[int, bool, bool]: 148 | good_termination = False 149 | bad_termination = False 150 | 151 | msgendidxplusone = msgstartidx + 1 152 | while msgendidxplusone <= endidx: 153 | # Look for a status byte 154 | # Second rule of the MIDI club is status bytes have MSB set 155 | if buf[msgendidxplusone] & 0x80: 156 | if buf[msgendidxplusone] == eom_status: 157 | good_termination = True 158 | else: 159 | bad_termination = True 160 | break 161 | msgendidxplusone += 1 162 | 163 | if good_termination or bad_termination: 164 | msgendidxplusone += 1 165 | 166 | return (msgendidxplusone, good_termination, bad_termination) 167 | 168 | @classmethod 169 | def _match_message_status( 170 | cls, buf: bytearray, msgstartidx: int, msgendidxplusone: int, endidx: int 171 | ) -> Tuple[Optional[Any], int, bool, bool, bool, int]: 172 | msgclass = None 173 | status = buf[msgstartidx] 174 | known_msg = False 175 | complete_msg = False 176 | bad_termination = False 177 | 178 | # Rummage through our list looking for a status match 179 | for status_mask, msgclass in MIDIMessage._statusandmask_to_class: 180 | masked_status = status & status_mask[1] 181 | if status_mask[0] == masked_status: 182 | known_msg = True 183 | # Check there's enough left to parse a complete message 184 | # this value can be changed later for a var. length msgs 185 | complete_msg = len(buf) - msgstartidx >= msgclass.LENGTH 186 | if not complete_msg: 187 | break 188 | 189 | if msgclass.LENGTH < 0: # indicator of variable length message 190 | ( 191 | msgendidxplusone, 192 | terminated_msg, 193 | bad_termination, 194 | ) = cls._search_eom_status( 195 | buf, msgclass.ENDSTATUS, msgstartidx, msgendidxplusone, endidx 196 | ) 197 | if not terminated_msg: 198 | complete_msg = False 199 | else: # fixed length message 200 | msgendidxplusone = msgstartidx + msgclass.LENGTH 201 | break 202 | 203 | return ( 204 | msgclass, 205 | status, 206 | known_msg, 207 | complete_msg, 208 | bad_termination, 209 | msgendidxplusone, 210 | ) 211 | 212 | @classmethod 213 | def from_message_bytes( 214 | cls, midibytes: bytearray, channel_in: Optional[Union[int, Tuple[int, ...]]] 215 | ) -> Tuple[Optional["MIDIMessage"], int, int]: 216 | """Create an appropriate object of the correct class for the 217 | first message found in some MIDI bytes filtered by channel_in. 218 | 219 | Returns (messageobject, endplusone, skipped) 220 | or for no messages, partial messages or messages for other channels 221 | (None, endplusone, skipped). 222 | """ 223 | endidx = len(midibytes) - 1 224 | skipped = 0 225 | preamble = True 226 | 227 | msgstartidx = 0 228 | msgendidxplusone = 0 229 | while True: 230 | msg = None 231 | # Look for a status byte 232 | # Second rule of the MIDI club is status bytes have MSB set 233 | while msgstartidx <= endidx and not midibytes[msgstartidx] & 0x80: 234 | msgstartidx += 1 235 | if preamble: 236 | skipped += 1 237 | preamble = False 238 | 239 | # Either no message or a partial one 240 | if msgstartidx > endidx: 241 | return (None, endidx + 1, skipped) 242 | 243 | # Try and match the status byte found in midibytes 244 | ( 245 | msgclass, 246 | status, 247 | known_message, 248 | complete_message, 249 | bad_termination, 250 | msgendidxplusone, 251 | ) = cls._match_message_status(midibytes, msgstartidx, msgendidxplusone, endidx) 252 | channel_match_orna = True 253 | if complete_message and not bad_termination: 254 | try: 255 | msg = msgclass.from_bytes(midibytes[msgstartidx:msgendidxplusone]) 256 | if msg.channel is not None: 257 | channel_match_orna = channel_filter(msg.channel, channel_in) 258 | 259 | except (ValueError, TypeError) as ex: 260 | msg = MIDIBadEvent(midibytes[msgstartidx:msgendidxplusone], ex) 261 | 262 | # break out of while loop for a complete message on good channel 263 | # or we have one we do not know about 264 | if known_message: 265 | if complete_message: 266 | if channel_match_orna: 267 | break 268 | # advance to next message 269 | msgstartidx = msgendidxplusone 270 | else: 271 | # Important case of a known message but one that is not 272 | # yet complete - leave bytes in buffer and wait for more 273 | break 274 | else: 275 | msg = MIDIUnknownEvent(status) 276 | # length cannot be known 277 | # next read will skip past leftover data bytes 278 | msgendidxplusone = msgstartidx + 1 279 | break 280 | 281 | return (msg, msgendidxplusone, skipped) 282 | 283 | # A default method for constructing wire messages with no data. 284 | # Returns an (immutable) bytes with just the status code in. 285 | def __bytes__(self) -> bytes: 286 | """Return the ``bytes`` wire protocol representation of the object 287 | with channel number applied where appropriate.""" 288 | return bytes([self._STATUS]) 289 | 290 | # databytes value present to keep interface uniform but unused 291 | # A default method for constructing message objects with no data. 292 | # Returns the new object. 293 | @classmethod 294 | def from_bytes(cls, msg_bytes: bytes) -> "MIDIMessage": 295 | """Creates an object from the byte stream of the wire protocol 296 | representation of the MIDI message.""" 297 | return cls() 298 | 299 | def __str__(self) -> str: 300 | """Print an instance""" 301 | cls = self.__class__ 302 | if slots := getattr(cls, "_message_slots", None): 303 | args = ", ".join(f"{name}={repr(getattr(self, name, None))}" for name in slots) 304 | else: 305 | args = "..." 306 | return f"{self.__class__.__name__}({args})" 307 | 308 | __repr__ = __str__ 309 | 310 | 311 | # DO NOT try to register these messages 312 | class MIDIUnknownEvent(MIDIMessage): 313 | """An unknown MIDI message. 314 | 315 | :param int status: The MIDI status number. 316 | 317 | This can either occur because there is no class representing the message 318 | or because it is not imported. 319 | """ 320 | 321 | _message_slots = ["status"] 322 | LENGTH = -1 323 | 324 | def __init__(self, status: int): 325 | self.status = status 326 | super().__init__() 327 | 328 | 329 | class MIDIBadEvent(MIDIMessage): 330 | """A bad MIDI message, one that could not be parsed/constructed. 331 | 332 | :param list msg_bytes: The MIDI status including any embedded channel number 333 | and associated subsequent data bytes. 334 | :param Exception exception: The exception used to store the repr() text representation. 335 | 336 | This could be due to status bytes appearing where data bytes are expected. 337 | The channel property will not be set. 338 | """ 339 | 340 | LENGTH = -1 341 | 342 | _message_slots = ["msg_bytes", "exception"] 343 | 344 | def __init__(self, msg_bytes: bytearray, exception: Exception): 345 | self.data = bytes(msg_bytes) 346 | self.exception_text = repr(exception) 347 | super().__init__() 348 | -------------------------------------------------------------------------------- /tests/test_MIDIMessage_unittests.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 Kevin J. Walters for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | import os 6 | import unittest 7 | 8 | verbose = int(os.getenv("TESTVERBOSE", "2")) 9 | 10 | # adafruit_midi had an import usb_midi 11 | import sys 12 | 13 | # sys.modules['usb_midi'] = MagicMock() 14 | 15 | # Borrowing the dhalbert/tannewt technique from adafruit/Adafruit_CircuitPython_Motor 16 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) 17 | 18 | # Import before messages - opposite to other test file 19 | import adafruit_midi 20 | 21 | # Full monty 22 | from adafruit_midi.note_off import NoteOff 23 | from adafruit_midi.note_on import NoteOn 24 | from adafruit_midi.system_exclusive import SystemExclusive 25 | 26 | 27 | class Test_MIDIMessage_from_message_byte_tests(unittest.TestCase): 28 | def test_NoteOn_basic(self): 29 | data = bytes([0x90, 0x30, 0x7F]) 30 | ichannel = 0 31 | 32 | (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes( 33 | data, ichannel 34 | ) 35 | 36 | self.assertIsInstance(msg, NoteOn) 37 | self.assertEqual(msg.note, 0x30) 38 | self.assertEqual(msg.velocity, 0x7F) 39 | self.assertEqual(msgendidxplusone, 3) 40 | self.assertEqual(skipped, 0) 41 | self.assertEqual(msg.channel, 0) 42 | 43 | def test_NoteOn_awaitingthirdbyte(self): 44 | data = bytes([0x90, 0x30]) 45 | ichannel = 0 46 | 47 | (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes( 48 | data, ichannel 49 | ) 50 | self.assertIsNone(msg) 51 | self.assertEqual( 52 | msgendidxplusone, 53 | skipped, 54 | "skipped must be 0 as it only indicates bytes before a status byte", 55 | ) 56 | self.assertEqual( 57 | msgendidxplusone, 58 | 0, 59 | "msgendidxplusone must be 0 as buffer must be lest as is for more data", 60 | ) 61 | self.assertEqual(skipped, 0) 62 | 63 | def test_NoteOn_predatajunk(self): 64 | data = bytes([0x20, 0x64, 0x90, 0x30, 0x32]) 65 | ichannel = 0 66 | 67 | (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes( 68 | data, ichannel 69 | ) 70 | 71 | self.assertIsInstance(msg, NoteOn) 72 | self.assertEqual(msg.note, 0x30) 73 | self.assertEqual(msg.velocity, 0x32) 74 | self.assertEqual( 75 | msgendidxplusone, 76 | 5, 77 | "data bytes from partial message and messages are removed", 78 | ) 79 | self.assertEqual(skipped, 2) 80 | self.assertEqual(msg.channel, 0) 81 | 82 | def test_NoteOn_prepartialsysex(self): 83 | data = bytes([0x01, 0x02, 0x03, 0x04, 0xF7, 0x90, 0x30, 0x32]) 84 | ichannel = 0 85 | 86 | (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes( 87 | data, ichannel 88 | ) 89 | 90 | # MIDIMessage parsing could be improved to return something that 91 | # indicates its a truncated end of SysEx 92 | self.assertIsInstance(msg, adafruit_midi.midi_message.MIDIUnknownEvent) 93 | self.assertEqual(msg.status, 0xF7) 94 | self.assertEqual( 95 | msgendidxplusone, 96 | 5, 97 | "removal of the end of the partial SysEx data and terminating status byte", 98 | ) 99 | self.assertEqual(skipped, 4, "skipped only counts data bytes so will be 4 here") 100 | self.assertIsNone(msg.channel) 101 | 102 | data = data[msgendidxplusone:] 103 | (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes( 104 | data, ichannel 105 | ) 106 | 107 | self.assertIsInstance( 108 | msg, 109 | NoteOn, 110 | "NoteOn is expected if SystemExclusive is loaded otherwise it'd be MIDIUnknownEvent", 111 | ) 112 | self.assertEqual(msg.note, 0x30) 113 | self.assertEqual(msg.velocity, 0x32) 114 | self.assertEqual(msgendidxplusone, 3, "NoteOn message removed") 115 | self.assertEqual(skipped, 0) 116 | self.assertEqual(msg.channel, 0) 117 | 118 | def test_NoteOn_postNoteOn(self): 119 | data = bytes([0x90 | 0x08, 0x30, 0x7F, 0x90 | 0x08, 0x37, 0x64]) 120 | ichannel = 8 121 | 122 | (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes( 123 | data, ichannel 124 | ) 125 | 126 | self.assertIsInstance(msg, NoteOn) 127 | self.assertEqual(msg.note, 0x30) 128 | self.assertEqual(msg.velocity, 0x7F) 129 | self.assertEqual(msgendidxplusone, 3) 130 | self.assertEqual(skipped, 0) 131 | self.assertEqual(msg.channel, 8) 132 | 133 | def test_NoteOn_postpartialNoteOn(self): 134 | data = bytes([0x90, 0x30, 0x7F, 0x90, 0x37]) 135 | ichannel = 0 136 | 137 | (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes( 138 | data, ichannel 139 | ) 140 | 141 | self.assertIsInstance(msg, NoteOn) 142 | self.assertEqual(msg.note, 0x30) 143 | self.assertEqual(msg.velocity, 0x7F) 144 | self.assertEqual(msgendidxplusone, 3, "Only first message is removed") 145 | self.assertEqual(skipped, 0) 146 | self.assertEqual(msg.channel, 0) 147 | 148 | def test_NoteOn_preotherchannel(self): 149 | data = bytes([0x90 | 0x05, 0x30, 0x7F, 0x90 | 0x03, 0x37, 0x64]) 150 | ichannel = 3 151 | 152 | (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes( 153 | data, ichannel 154 | ) 155 | 156 | self.assertIsInstance(msg, NoteOn) 157 | self.assertEqual(msg.note, 0x37) 158 | self.assertEqual(msg.velocity, 0x64) 159 | self.assertEqual(msgendidxplusone, 6, "Both messages are removed from buffer") 160 | self.assertEqual(skipped, 0) 161 | self.assertEqual(msg.channel, 3) 162 | 163 | def test_NoteOn_preotherchannelplusintermediatejunk( 164 | self, 165 | ): 166 | data = bytes([0x90 | 0x05, 0x30, 0x7F, 0x00, 0x00, 0x90 | 0x03, 0x37, 0x64]) 167 | ichannel = 3 168 | 169 | (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes( 170 | data, ichannel 171 | ) 172 | 173 | self.assertIsInstance(msg, NoteOn) 174 | self.assertEqual(msg.note, 0x37) 175 | self.assertEqual(msg.velocity, 0x64) 176 | self.assertEqual(msgendidxplusone, 8, "Both messages and junk are removed from buffer") 177 | self.assertEqual(skipped, 0) 178 | self.assertEqual(msg.channel, 3) 179 | 180 | def test_NoteOn_wrongchannel(self): 181 | data = bytes([0x95, 0x30, 0x7F]) 182 | ichannel = 3 183 | 184 | (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes( 185 | data, ichannel 186 | ) 187 | 188 | self.assertIsNone(msg) 189 | self.assertEqual(msgendidxplusone, 3, "wrong channel message discarded") 190 | self.assertEqual(skipped, 0) 191 | 192 | def test_NoteOn_partialandpreotherchannel1(self): 193 | data = bytes([0x95, 0x30, 0x7F, 0x93]) 194 | ichannel = 3 195 | 196 | (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes( 197 | data, ichannel 198 | ) 199 | 200 | self.assertIsNone(msg) 201 | self.assertEqual(msgendidxplusone, 3, "first message discarded, second partial left") 202 | self.assertEqual(skipped, 0) 203 | 204 | def test_NoteOn_partialandpreotherchannel2(self): 205 | data = bytes([0x95, 0x30, 0x7F, 0x93, 0x37]) 206 | ichannel = 3 207 | 208 | (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes( 209 | data, ichannel 210 | ) 211 | 212 | self.assertIsNone(msg) 213 | self.assertEqual(msgendidxplusone, 3, "first message discarded, second partial left") 214 | self.assertEqual(skipped, 0) 215 | 216 | def test_NoteOn_constructor_int(self): 217 | object1 = NoteOn(60, 0x7F) 218 | 219 | self.assertEqual(object1.note, 60) 220 | self.assertEqual(object1.velocity, 0x7F) 221 | self.assertIsNone(object1.channel) 222 | 223 | object2 = NoteOn(60, 0x00) # equivalent of NoteOff 224 | 225 | self.assertEqual(object2.note, 60) 226 | self.assertEqual(object2.velocity, 0x00) 227 | self.assertIsNone(object2.channel) 228 | 229 | object3 = NoteOn(60, 0x50, channel=7) 230 | 231 | self.assertEqual(object3.note, 60) 232 | self.assertEqual(object3.velocity, 0x50) 233 | self.assertEqual(object3.channel, 7) 234 | 235 | object4 = NoteOn(60) # velocity defaults to 127 236 | 237 | self.assertEqual(object4.note, 60) 238 | self.assertEqual(object4.velocity, 127) 239 | self.assertIsNone(object4.channel) 240 | 241 | def test_SystemExclusive_NoteOn(self): 242 | data = bytes([0xF0, 0x42, 0x01, 0x02, 0x03, 0x04, 0xF7, 0x90 | 14, 0x30, 0x60]) 243 | ichannel = 14 244 | 245 | (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes( 246 | data, ichannel 247 | ) 248 | 249 | self.assertIsInstance(msg, SystemExclusive) 250 | self.assertEqual(msg.manufacturer_id, bytes([0x42])) # Korg 251 | self.assertEqual(msg.data, bytes([0x01, 0x02, 0x03, 0x04])) 252 | self.assertEqual(msgendidxplusone, 7) 253 | self.assertEqual(skipped, 0, "If SystemExclusive class is imported then this must be 0") 254 | self.assertIsNone(msg.channel) 255 | 256 | (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes( 257 | data[msgendidxplusone:], ichannel 258 | ) 259 | 260 | self.assertIsInstance(msg, NoteOn) 261 | self.assertEqual(msg.note, 48) 262 | self.assertEqual(msg.velocity, 0x60) 263 | self.assertEqual(msgendidxplusone, 3) 264 | self.assertEqual(skipped, 0) 265 | self.assertEqual(msg.channel, 14) 266 | 267 | def test_SystemExclusive_NoteOn_premalterminatedsysex( 268 | self, 269 | ): 270 | data = bytes([0xF0, 0x42, 0x01, 0x02, 0x03, 0x04, 0xF0, 0x90, 0x30, 0x32]) 271 | ichannel = 0 272 | 273 | # 0xf0 is incorrect status to mark end of this message, must be 0xf7 274 | (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes( 275 | data, ichannel 276 | ) 277 | 278 | self.assertIsNone(msg) 279 | self.assertEqual(msgendidxplusone, 7) 280 | self.assertEqual(skipped, 0, "If SystemExclusive class is imported then this must be 0") 281 | 282 | def test_Unknown_SinglebyteStatus(self): 283 | data = bytes([0xFD]) 284 | ichannel = 0 285 | 286 | (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes( 287 | data, ichannel 288 | ) 289 | 290 | self.assertIsInstance(msg, adafruit_midi.midi_message.MIDIUnknownEvent) 291 | self.assertEqual(msgendidxplusone, 1) 292 | self.assertEqual(skipped, 0) 293 | self.assertIsNone(msg.channel) 294 | 295 | def test_Empty(self): 296 | data = bytes([]) 297 | ichannel = 0 298 | 299 | (msg, msgendidxplusone, skipped) = adafruit_midi.MIDIMessage.from_message_bytes( 300 | data, ichannel 301 | ) 302 | 303 | self.assertIsNone(msg) 304 | self.assertEqual(msgendidxplusone, 0) 305 | self.assertEqual(skipped, 0) 306 | 307 | 308 | class Test_MIDIMessage_NoteOn_constructor(unittest.TestCase): 309 | def test_NoteOn_constructor_string(self): 310 | object1 = NoteOn("C4", 0x64) 311 | self.assertEqual(object1.note, 60) 312 | self.assertEqual(object1.velocity, 0x64) 313 | 314 | object2 = NoteOn("C3", 0x7F) 315 | self.assertEqual(object2.note, 48) 316 | self.assertEqual(object2.velocity, 0x7F) 317 | 318 | object3 = NoteOn("C#4", 0x00) 319 | self.assertEqual(object3.note, 61) 320 | self.assertEqual(object3.velocity, 0) 321 | 322 | def test_NoteOn_constructor_valueerror1(self): 323 | with self.assertRaises(ValueError): 324 | NoteOn(60, 0x80) 325 | 326 | def test_NoteOn_constructor_valueerror2(self): 327 | with self.assertRaises(ValueError): 328 | NoteOn(-1, 0x7F) 329 | 330 | def test_NoteOn_constructor_valueerror3(self): 331 | with self.assertRaises(ValueError): 332 | NoteOn(128, 0x7F) 333 | 334 | def test_NoteOn_constructor_upperrange1(self): 335 | object1 = NoteOn("G9", 0x7F) 336 | self.assertEqual(object1.note, 127) 337 | self.assertEqual(object1.velocity, 0x7F) 338 | 339 | def test_NoteOn_constructor_upperrange2(self): 340 | with self.assertRaises(ValueError): 341 | NoteOn("G#9", 0x7F) # just above max note 342 | 343 | def test_NoteOn_constructor_bogusstring(self): 344 | with self.assertRaises(ValueError): 345 | NoteOn("CC4", 0x7F) 346 | 347 | 348 | class Test_MIDIMessage_NoteOff_constructor(unittest.TestCase): 349 | # mostly cut and paste from NoteOn above 350 | def test_NoteOff_constructor_string(self): 351 | object1 = NoteOff("C4", 0x64) 352 | self.assertEqual(object1.note, 60) 353 | self.assertEqual(object1.velocity, 0x64) 354 | 355 | object2 = NoteOff("C3", 0x7F) 356 | self.assertEqual(object2.note, 48) 357 | self.assertEqual(object2.velocity, 0x7F) 358 | 359 | object3 = NoteOff("C#4", 0x00) 360 | self.assertEqual(object3.note, 61) 361 | self.assertEqual(object3.velocity, 0) 362 | 363 | object4 = NoteOff("C#4") # velocity defaults to 0 364 | self.assertEqual(object4.note, 61) 365 | self.assertEqual(object4.velocity, 0) 366 | 367 | def test_NoteOff_constructor_valueerror1(self): 368 | with self.assertRaises(ValueError): 369 | NoteOff(60, 0x80) 370 | 371 | def test_NoteOff_constructor_valueerror2(self): 372 | with self.assertRaises(ValueError): 373 | NoteOff(-1, 0x7F) 374 | 375 | def test_NoteOff_constructor_valueerror3(self): 376 | with self.assertRaises(ValueError): 377 | NoteOff(128, 0x7F) 378 | 379 | def test_NoteOff_constructor_upperrange1(self): 380 | object1 = NoteOff("G9", 0x7F) 381 | self.assertEqual(object1.note, 127) 382 | self.assertEqual(object1.velocity, 0x7F) 383 | 384 | def test_NoteOff_constructor_upperrange2(self): 385 | with self.assertRaises(ValueError): 386 | NoteOff("G#9", 0x7F) # just above max note 387 | 388 | def test_NoteOff_constructor_bogusstring(self): 389 | with self.assertRaises(ValueError): 390 | NoteOff("CC4", 0x7F) 391 | 392 | 393 | if __name__ == "__main__": 394 | unittest.main(verbosity=verbose) 395 | -------------------------------------------------------------------------------- /tests/test_MIDI_unittests.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 Kevin J. Walters for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | import os 6 | import random 7 | import unittest 8 | from unittest.mock import Mock, call 9 | 10 | verbose = int(os.getenv("TESTVERBOSE", "2")) 11 | 12 | # adafruit_midi had an import usb_midi 13 | import sys 14 | 15 | # sys.modules['usb_midi'] = MagicMock() 16 | 17 | # Borrowing the dhalbert/tannewt technique from adafruit/Adafruit_CircuitPython_Motor 18 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) 19 | 20 | # Import after messages - opposite to other test file 21 | import adafruit_midi 22 | 23 | # Full monty 24 | from adafruit_midi.channel_pressure import ChannelPressure 25 | from adafruit_midi.control_change import ControlChange 26 | from adafruit_midi.note_off import NoteOff 27 | from adafruit_midi.note_on import NoteOn 28 | from adafruit_midi.pitch_bend import PitchBend 29 | from adafruit_midi.system_exclusive import SystemExclusive 30 | 31 | 32 | # For loopback/echo tests 33 | def MIDI_mocked_both_loopback(in_c, out_c): 34 | usb_data = bytearray() 35 | 36 | def write(buffer, length): 37 | nonlocal usb_data 38 | usb_data.extend(buffer[0:length]) 39 | 40 | def read(length): 41 | nonlocal usb_data 42 | poppedbytes = usb_data[0:length] 43 | usb_data = usb_data[len(poppedbytes) :] 44 | return bytes(poppedbytes) 45 | 46 | mockedportin = Mock() 47 | mockedportin.read = read 48 | mockedportout = Mock() 49 | mockedportout.write = write 50 | midi = adafruit_midi.MIDI( 51 | midi_out=mockedportout, midi_in=mockedportin, out_channel=out_c, in_channel=in_c 52 | ) 53 | return midi 54 | 55 | 56 | def MIDI_mocked_receive(in_c, data, read_sizes): 57 | usb_data = bytearray(data) 58 | chunks = read_sizes 59 | chunk_idx = 0 60 | 61 | def read(length): 62 | nonlocal usb_data, chunks, chunk_idx 63 | if length != 0 and chunk_idx < len(chunks): 64 | # min() to ensure we only read what's asked for and present 65 | poppedbytes = usb_data[0 : min(length, chunks[chunk_idx])] 66 | usb_data = usb_data[len(poppedbytes) :] 67 | if length >= chunks[chunk_idx]: 68 | chunk_idx += 1 69 | else: 70 | chunks[chunk_idx] -= len(poppedbytes) 71 | return bytes(poppedbytes) 72 | else: 73 | return b"" 74 | 75 | mockedportin = Mock() 76 | mockedportin.read = read 77 | 78 | midi = adafruit_midi.MIDI( 79 | midi_out=None, midi_in=mockedportin, out_channel=in_c, in_channel=in_c 80 | ) 81 | return midi 82 | 83 | 84 | class Test_MIDI_constructor(unittest.TestCase): 85 | def test_no_inout(self): 86 | # constructor likes a bit of in out 87 | with self.assertRaises(ValueError): 88 | adafruit_midi.MIDI() 89 | 90 | 91 | class Test_MIDI(unittest.TestCase): 92 | def test_captured_data_one_byte_reads(self): 93 | channel = 0 94 | # From an M-Audio AXIOM controller 95 | raw_data = bytearray( 96 | [0x90, 0x3E, 0x5F] 97 | + [0xD0, 0x10] 98 | + [0x90, 0x40, 0x66] 99 | + [0xB0, 0x1, 0x08] 100 | + [0x90, 0x41, 0x74] 101 | + [0xE0, 0x03, 0x40] 102 | ) 103 | midi = MIDI_mocked_receive(channel, raw_data, [1] * len(raw_data)) 104 | 105 | for unused in range(100): 106 | msg = midi.receive() 107 | if msg is not None: 108 | break 109 | self.assertIsInstance(msg, NoteOn) 110 | self.assertEqual(msg.note, 0x3E) 111 | self.assertEqual(msg.velocity, 0x5F) 112 | self.assertEqual(msg.channel, channel) 113 | 114 | # for loops currently absorb any Nones but could 115 | # be set to read precisely the expected number... 116 | for unused in range(100): 117 | msg = midi.receive() 118 | if msg is not None: 119 | break 120 | self.assertIsInstance(msg, ChannelPressure) 121 | self.assertEqual(msg.pressure, 0x10) 122 | self.assertEqual(msg.channel, channel) 123 | 124 | for unused in range(100): 125 | msg = midi.receive() 126 | if msg is not None: 127 | break 128 | self.assertIsInstance(msg, NoteOn) 129 | self.assertEqual(msg.note, 0x40) 130 | self.assertEqual(msg.velocity, 0x66) 131 | self.assertEqual(msg.channel, channel) 132 | 133 | for unused in range(100): 134 | msg = midi.receive() 135 | if msg is not None: 136 | break 137 | self.assertIsInstance(msg, ControlChange) 138 | self.assertEqual(msg.control, 0x01) 139 | self.assertEqual(msg.value, 0x08) 140 | self.assertEqual(msg.channel, channel) 141 | 142 | for unused in range(100): 143 | msg = midi.receive() 144 | if msg is not None: 145 | break 146 | self.assertIsInstance(msg, NoteOn) 147 | self.assertEqual(msg.note, 0x41) 148 | self.assertEqual(msg.velocity, 0x74) 149 | self.assertEqual(msg.channel, channel) 150 | 151 | for unused in range(100): 152 | msg = midi.receive() 153 | if msg is not None: 154 | break 155 | self.assertIsInstance(msg, PitchBend) 156 | self.assertEqual(msg.pitch_bend, 8195) 157 | self.assertEqual(msg.channel, channel) 158 | 159 | for unused in range(100): 160 | msg = midi.receive() 161 | self.assertIsNone(msg) 162 | 163 | def test_unknown_before_NoteOn(self): 164 | channel = 0 165 | # From an M-Audio AXIOM controller 166 | raw_data = bytes( 167 | [0b11110011, 0x10] # Song Select (not yet implemented) 168 | + [0b11110011, 0x20] 169 | + [0b11110100] 170 | + [0b11110101] 171 | ) + bytes(NoteOn("C5", 0x7F, channel=channel)) 172 | midi = MIDI_mocked_receive(channel, raw_data, [2, 2, 1, 1, 3]) 173 | 174 | for _ in range(4): 175 | msg = midi.receive() 176 | self.assertIsInstance(msg, adafruit_midi.midi_message.MIDIUnknownEvent) 177 | self.assertIsNone(msg.channel) 178 | 179 | msg = midi.receive() 180 | self.assertIsInstance(msg, NoteOn) 181 | self.assertEqual(msg.note, 0x48) # 0x48 is C5 182 | self.assertEqual(msg.velocity, 0x7F) 183 | self.assertEqual(msg.channel, channel) 184 | 185 | # See https://github.com/adafruit/Adafruit_CircuitPython_MIDI/issues/8 186 | def test_running_status_when_implemented(self): 187 | channel = 8 188 | raw_data = ( 189 | bytes(NoteOn("C5", 0x7F, channel=channel)) 190 | + bytes([0xE8, 0x72, 0x40] + [0x6D, 0x40] + [0x05, 0x41]) 191 | + bytes(NoteOn("D5", 0x7F, channel=channel)) 192 | ) 193 | 194 | midi = MIDI_mocked_receive(channel, raw_data, [3 + 3 + 2 + 3 + 3]) 195 | self.assertIsInstance(midi, adafruit_midi.MIDI) 196 | # self.assertEqual(TOFINISH, WHENIMPLEMENTED) 197 | 198 | def test_somegood_somemissing_databytes(self): 199 | channel = 8 200 | raw_data = ( 201 | bytes(NoteOn("C5", 0x7F, channel=channel)) 202 | + bytes( 203 | [0xE8, 0x72, 0x40] 204 | + [0xE8, 0x6D] # Missing last data byte 205 | + [0xE8, 0x5, 0x41] 206 | ) 207 | + bytes(NoteOn("D5", 0x7F, channel=channel)) 208 | ) 209 | midi = MIDI_mocked_receive(channel, raw_data, [3 + 3 + 2 + 3 + 3]) 210 | 211 | msg1 = midi.receive() 212 | self.assertIsInstance(msg1, NoteOn) 213 | self.assertEqual(msg1.note, 72) 214 | self.assertEqual(msg1.velocity, 0x7F) 215 | self.assertEqual(msg1.channel, channel) 216 | 217 | msg2 = midi.receive() 218 | self.assertIsInstance(msg2, PitchBend) 219 | self.assertEqual(msg2.pitch_bend, 8306) 220 | self.assertEqual(msg2.channel, channel) 221 | 222 | # The current implementation will read status bytes for data 223 | # In most cases it would be a faster recovery with fewer messages 224 | # lost if the next status byte wasn't consumed 225 | # and parsing restarted from that byte 226 | msg3 = midi.receive() 227 | self.assertIsInstance(msg3, adafruit_midi.midi_message.MIDIBadEvent) 228 | self.assertIsInstance(msg3.data, bytes) 229 | self.assertEqual(msg3.data, bytes([0xE8, 0x6D, 0xE8])) 230 | self.assertIsNone(msg3.channel) 231 | 232 | # (msg4, channel4) = m.receive() 233 | # self.assertIsInstance(msg4, PitchBend) 234 | # self.assertEqual(msg4.pitch_bend, 72) 235 | # self.assertEqual(channel4, c) 236 | 237 | msg5 = midi.receive() 238 | self.assertIsInstance(msg5, NoteOn) 239 | self.assertEqual(msg5.note, 74) 240 | self.assertEqual(msg5.velocity, 0x7F) 241 | self.assertEqual(msg5.channel, channel) 242 | 243 | msg6 = midi.receive() 244 | self.assertIsNone(msg6) 245 | 246 | def test_smallsysex_between_notes(self): 247 | midi = MIDI_mocked_both_loopback(3, 3) 248 | 249 | midi.send( 250 | [ 251 | NoteOn("C4", 0x7F), 252 | SystemExclusive([0x1F], [1, 2, 3, 4, 5, 6, 7, 8]), 253 | NoteOff(60, 0x28), 254 | ] 255 | ) 256 | 257 | msg1 = midi.receive() 258 | self.assertIsInstance(msg1, NoteOn) 259 | self.assertEqual(msg1.note, 60) 260 | self.assertEqual(msg1.velocity, 0x7F) 261 | self.assertEqual(msg1.channel, 3) 262 | 263 | msg2 = midi.receive() 264 | self.assertIsInstance(msg2, SystemExclusive) 265 | self.assertEqual(msg2.manufacturer_id, bytes([0x1F])) 266 | self.assertEqual(msg2.data, bytes([1, 2, 3, 4, 5, 6, 7, 8])) 267 | self.assertEqual(msg2.channel, None) # SysEx does not have a channel 268 | 269 | msg3 = midi.receive() 270 | self.assertIsInstance(msg3, NoteOff) 271 | self.assertEqual(msg3.note, 60) 272 | self.assertEqual(msg3.velocity, 0x28) 273 | self.assertEqual(msg3.channel, 3) 274 | 275 | msg4 = midi.receive() 276 | self.assertIsNone(msg4) 277 | 278 | def test_smallsysex_bytes_type(self): 279 | message = SystemExclusive([0x1F], [100, 150, 200]) 280 | 281 | self.assertIsInstance(message, SystemExclusive) 282 | self.assertEqual(message.manufacturer_id, bytes([0x1F])) 283 | self.assertIsInstance(message.manufacturer_id, bytes) 284 | 285 | # check this really is immutable 286 | with self.assertRaises(TypeError): 287 | message.data[0] = 0 288 | 289 | self.assertEqual(message.data, bytes([100, 150, 200])) 290 | self.assertIsInstance(message.data, bytes) 291 | 292 | def test_larger_than_buffer_sysex(self): 293 | channel = 0 294 | monster_data_len = 500 295 | raw_data = ( 296 | bytes(NoteOn("C5", 0x7F, channel=channel)) 297 | + bytes(SystemExclusive([0x02], [d & 0x7F for d in range(monster_data_len)])) 298 | + bytes(NoteOn("D5", 0x7F, channel=channel)) 299 | ) 300 | midi = MIDI_mocked_receive(channel, raw_data, [len(raw_data)]) 301 | buffer_len = midi._in_buf_size 302 | self.assertTrue( 303 | monster_data_len > buffer_len, 304 | "checking our SysEx truly is larger than buffer", 305 | ) 306 | 307 | msg1 = midi.receive() 308 | self.assertIsInstance(msg1, NoteOn) 309 | self.assertEqual(msg1.note, 72) 310 | self.assertEqual(msg1.velocity, 0x7F) 311 | self.assertEqual(msg1.channel, channel) 312 | 313 | # (Ab)using python's rounding down for negative division 314 | for unused in range(-(-(1 + 1 + monster_data_len + 1) // buffer_len) - 1): 315 | msg2 = midi.receive() 316 | self.assertIsNone(msg2) 317 | 318 | # The current implementation will read SysEx end status byte 319 | # and report it as an unknown 320 | msg3 = midi.receive() 321 | self.assertIsInstance(msg3, adafruit_midi.midi_message.MIDIUnknownEvent) 322 | self.assertEqual(msg3.status, 0xF7) 323 | self.assertIsNone(msg3.channel) 324 | 325 | # (msg4, channel4) = m.receive() 326 | # self.assertIsInstance(msg4, PitchBend) 327 | # self.assertEqual(msg4.pitch_bend, 72) 328 | # self.assertEqual(channel4, c) 329 | 330 | msg5 = midi.receive() 331 | self.assertIsInstance(msg5, NoteOn) 332 | self.assertEqual(msg5.note, 74) 333 | self.assertEqual(msg5.velocity, 0x7F) 334 | self.assertEqual(msg5.channel, channel) 335 | 336 | msg6 = midi.receive() 337 | self.assertIsNone(msg6) 338 | 339 | 340 | # mock_calls handling 341 | class Test_MIDI_send(unittest.TestCase): 342 | def test_send_basic_single(self): 343 | # def printit(buffer, len): 344 | # print(buffer[0:len]) 345 | mockedportout = Mock() 346 | # mockedPortOut.write = printit 347 | 348 | midi = adafruit_midi.MIDI(midi_out=mockedportout, out_channel=2) 349 | 350 | # Test sending some NoteOn and NoteOff to various channels 351 | nextcall = 0 352 | midi.send(NoteOn(0x60, 0x7F)) 353 | self.assertEqual(mockedportout.write.mock_calls[nextcall], call(b"\x92\x60\x7f", 3)) 354 | nextcall += 1 355 | midi.send(NoteOn(0x64, 0x3F)) 356 | self.assertEqual(mockedportout.write.mock_calls[nextcall], call(b"\x92\x64\x3f", 3)) 357 | nextcall += 1 358 | midi.send(NoteOn(0x67, 0x1F)) 359 | self.assertEqual(mockedportout.write.mock_calls[nextcall], call(b"\x92\x67\x1f", 3)) 360 | nextcall += 1 361 | 362 | midi.send(NoteOn(0x60, 0x00)) # Alternative to NoteOff 363 | self.assertEqual(mockedportout.write.mock_calls[nextcall], call(b"\x92\x60\x00", 3)) 364 | nextcall += 1 365 | midi.send(NoteOff(0x64, 0x01)) 366 | self.assertEqual(mockedportout.write.mock_calls[nextcall], call(b"\x82\x64\x01", 3)) 367 | nextcall += 1 368 | midi.send(NoteOff(0x67, 0x02)) 369 | self.assertEqual(mockedportout.write.mock_calls[nextcall], call(b"\x82\x67\x02", 3)) 370 | nextcall += 1 371 | 372 | # Setting channel to non default 373 | midi.send(NoteOn(0x6C, 0x7F), channel=9) 374 | self.assertEqual(mockedportout.write.mock_calls[nextcall], call(b"\x99\x6c\x7f", 3)) 375 | nextcall += 1 376 | 377 | midi.send(NoteOff(0x6C, 0x7F), channel=9) 378 | self.assertEqual(mockedportout.write.mock_calls[nextcall], call(b"\x89\x6c\x7f", 3)) 379 | nextcall += 1 380 | 381 | def test_send_badnotes(self): 382 | mockedportout = Mock() 383 | 384 | midi = adafruit_midi.MIDI(midi_out=mockedportout, out_channel=2) 385 | 386 | # Test sending some NoteOn and NoteOff to various channels 387 | nextcall = 0 388 | midi.send(NoteOn(60, 0x7F)) 389 | self.assertEqual(mockedportout.write.mock_calls[nextcall], call(b"\x92\x3c\x7f", 3)) 390 | nextcall += 1 391 | with self.assertRaises(ValueError): 392 | midi.send(NoteOn(64, 0x80)) # Velocity > 127 - illegal value 393 | 394 | with self.assertRaises(ValueError): 395 | midi.send(NoteOn(67, -1)) 396 | 397 | # test after exceptions to ensure sending is still ok 398 | midi.send(NoteOn(72, 0x7F)) 399 | self.assertEqual(mockedportout.write.mock_calls[nextcall], call(b"\x92\x48\x7f", 3)) 400 | nextcall += 1 401 | 402 | def test_send_basic_sequences(self): 403 | # def printit(buffer, len): 404 | # print(buffer[0:len]) 405 | mockedportout = Mock() 406 | # mockedportout.write = printit 407 | 408 | midi = adafruit_midi.MIDI(midi_out=mockedportout, out_channel=2) 409 | 410 | # Test sending some NoteOn and NoteOff to various channels 411 | nextcall = 0 412 | # Test sequences with list syntax and pass a tuple too 413 | note_list = [NoteOn(0x6C, 0x51), NoteOn(0x70, 0x52), NoteOn(0x73, 0x53)] 414 | note_tuple = tuple(note_list) 415 | midi.send(note_list, channel=10) 416 | self.assertEqual( 417 | mockedportout.write.mock_calls[nextcall], 418 | call(b"\x9a\x6c\x51\x9a\x70\x52\x9a\x73\x53", 9), 419 | "The implementation writes in one go, single 9 byte write expected", 420 | ) 421 | nextcall += 1 422 | midi.send(note_tuple, channel=11) 423 | self.assertEqual( 424 | mockedportout.write.mock_calls[nextcall], 425 | call(b"\x9b\x6c\x51\x9b\x70\x52\x9b\x73\x53", 9), 426 | "The implementation writes in one go, single 9 byte write expected", 427 | ) 428 | nextcall += 1 429 | 430 | def test_termination_with_random_data(self): 431 | """Test with a random stream of bytes to ensure that the parsing code 432 | termates and returns, i.e. does not go into any infinite loops. 433 | """ 434 | channel = 0 435 | random.seed(303808) 436 | raw_data = bytearray([random.randint(0, 255) for i in range(50000)]) 437 | midi = MIDI_mocked_receive(channel, raw_data, [len(raw_data)]) 438 | 439 | noinfiniteloops = False 440 | for unused in range(len(raw_data)): 441 | midi.receive() # not interested in returned tuple 442 | 443 | noinfiniteloops = True # interested in getting to here 444 | self.assertTrue(noinfiniteloops) 445 | 446 | 447 | if __name__ == "__main__": 448 | unittest.main(verbosity=verbose) 449 | -------------------------------------------------------------------------------- /LICENSES/CC-BY-4.0.txt: -------------------------------------------------------------------------------- 1 | Creative Commons Attribution 4.0 International Creative Commons Corporation 2 | ("Creative Commons") is not a law firm and does not provide legal services 3 | or legal advice. Distribution of Creative Commons public licenses does not 4 | create a lawyer-client or other relationship. Creative Commons makes its licenses 5 | and related information available on an "as-is" basis. Creative Commons gives 6 | no warranties regarding its licenses, any material licensed under their terms 7 | and conditions, or any related information. Creative Commons disclaims all 8 | liability for damages resulting from their use to the fullest extent possible. 9 | 10 | Using Creative Commons Public Licenses 11 | 12 | Creative Commons public licenses provide a standard set of terms and conditions 13 | that creators and other rights holders may use to share original works of 14 | authorship and other material subject to copyright and certain other rights 15 | specified in the public license below. The following considerations are for 16 | informational purposes only, are not exhaustive, and do not form part of our 17 | licenses. 18 | 19 | Considerations for licensors: Our public licenses are intended for use by 20 | those authorized to give the public permission to use material in ways otherwise 21 | restricted by copyright and certain other rights. Our licenses are irrevocable. 22 | Licensors should read and understand the terms and conditions of the license 23 | they choose before applying it. Licensors should also secure all rights necessary 24 | before applying our licenses so that the public can reuse the material as 25 | expected. Licensors should clearly mark any material not subject to the license. 26 | This includes other CC-licensed material, or material used under an exception 27 | or limitation to copyright. More considerations for licensors : wiki.creativecommons.org/Considerations_for_licensors 28 | 29 | Considerations for the public: By using one of our public licenses, a licensor 30 | grants the public permission to use the licensed material under specified 31 | terms and conditions. If the licensor's permission is not necessary for any 32 | reason–for example, because of any applicable exception or limitation to copyright–then 33 | that use is not regulated by the license. Our licenses grant only permissions 34 | under copyright and certain other rights that a licensor has authority to 35 | grant. Use of the licensed material may still be restricted for other reasons, 36 | including because others have copyright or other rights in the material. A 37 | licensor may make special requests, such as asking that all changes be marked 38 | or described. Although not required by our licenses, you are encouraged to 39 | respect those requests where reasonable. More considerations for the public 40 | : wiki.creativecommons.org/Considerations_for_licensees Creative Commons Attribution 41 | 4.0 International Public License 42 | 43 | By exercising the Licensed Rights (defined below), You accept and agree to 44 | be bound by the terms and conditions of this Creative Commons Attribution 45 | 4.0 International Public License ("Public License"). To the extent this Public 46 | License may be interpreted as a contract, You are granted the Licensed Rights 47 | in consideration of Your acceptance of these terms and conditions, and the 48 | Licensor grants You such rights in consideration of benefits the Licensor 49 | receives from making the Licensed Material available under these terms and 50 | conditions. 51 | 52 | Section 1 – Definitions. 53 | 54 | a. Adapted Material means material subject to Copyright and Similar Rights 55 | that is derived from or based upon the Licensed Material and in which the 56 | Licensed Material is translated, altered, arranged, transformed, or otherwise 57 | modified in a manner requiring permission under the Copyright and Similar 58 | Rights held by the Licensor. For purposes of this Public License, where the 59 | Licensed Material is a musical work, performance, or sound recording, Adapted 60 | Material is always produced where the Licensed Material is synched in timed 61 | relation with a moving image. 62 | 63 | b. Adapter's License means the license You apply to Your Copyright and Similar 64 | Rights in Your contributions to Adapted Material in accordance with the terms 65 | and conditions of this Public License. 66 | 67 | c. Copyright and Similar Rights means copyright and/or similar rights closely 68 | related to copyright including, without limitation, performance, broadcast, 69 | sound recording, and Sui Generis Database Rights, without regard to how the 70 | rights are labeled or categorized. For purposes of this Public License, the 71 | rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. 72 | 73 | d. Effective Technological Measures means those measures that, in the absence 74 | of proper authority, may not be circumvented under laws fulfilling obligations 75 | under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, 76 | and/or similar international agreements. 77 | 78 | e. Exceptions and Limitations means fair use, fair dealing, and/or any other 79 | exception or limitation to Copyright and Similar Rights that applies to Your 80 | use of the Licensed Material. 81 | 82 | f. Licensed Material means the artistic or literary work, database, or other 83 | material to which the Licensor applied this Public License. 84 | 85 | g. Licensed Rights means the rights granted to You subject to the terms and 86 | conditions of this Public License, which are limited to all Copyright and 87 | Similar Rights that apply to Your use of the Licensed Material and that the 88 | Licensor has authority to license. 89 | 90 | h. Licensor means the individual(s) or entity(ies) granting rights under this 91 | Public License. 92 | 93 | i. Share means to provide material to the public by any means or process that 94 | requires permission under the Licensed Rights, such as reproduction, public 95 | display, public performance, distribution, dissemination, communication, or 96 | importation, and to make material available to the public including in ways 97 | that members of the public may access the material from a place and at a time 98 | individually chosen by them. 99 | 100 | j. Sui Generis Database Rights means rights other than copyright resulting 101 | from Directive 96/9/EC of the European Parliament and of the Council of 11 102 | March 1996 on the legal protection of databases, as amended and/or succeeded, 103 | as well as other essentially equivalent rights anywhere in the world. 104 | 105 | k. You means the individual or entity exercising the Licensed Rights under 106 | this Public License. Your has a corresponding meaning. 107 | 108 | Section 2 – Scope. 109 | 110 | a. License grant. 111 | 112 | 1. Subject to the terms and conditions of this Public License, the Licensor 113 | hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, 114 | irrevocable license to exercise the Licensed Rights in the Licensed Material 115 | to: 116 | 117 | A. reproduce and Share the Licensed Material, in whole or in part; and 118 | 119 | B. produce, reproduce, and Share Adapted Material. 120 | 121 | 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions 122 | and Limitations apply to Your use, this Public License does not apply, and 123 | You do not need to comply with its terms and conditions. 124 | 125 | 3. Term. The term of this Public License is specified in Section 6(a). 126 | 127 | 4. Media and formats; technical modifications allowed. The Licensor authorizes 128 | You to exercise the Licensed Rights in all media and formats whether now known 129 | or hereafter created, and to make technical modifications necessary to do 130 | so. The Licensor waives and/or agrees not to assert any right or authority 131 | to forbid You from making technical modifications necessary to exercise the 132 | Licensed Rights, including technical modifications necessary to circumvent 133 | Effective Technological Measures. For purposes of this Public License, simply 134 | making modifications authorized by this Section 2(a)(4) never produces Adapted 135 | Material. 136 | 137 | 5. Downstream recipients. 138 | 139 | A. Offer from the Licensor – Licensed Material. Every recipient of the Licensed 140 | Material automatically receives an offer from the Licensor to exercise the 141 | Licensed Rights under the terms and conditions of this Public License. 142 | 143 | B. No downstream restrictions. You may not offer or impose any additional 144 | or different terms or conditions on, or apply any Effective Technological 145 | Measures to, the Licensed Material if doing so restricts exercise of the Licensed 146 | Rights by any recipient of the Licensed Material. 147 | 148 | 6. No endorsement. Nothing in this Public License constitutes or may be construed 149 | as permission to assert or imply that You are, or that Your use of the Licensed 150 | Material is, connected with, or sponsored, endorsed, or granted official status 151 | by, the Licensor or others designated to receive attribution as provided in 152 | Section 3(a)(1)(A)(i). 153 | 154 | b. Other rights. 155 | 156 | 1. Moral rights, such as the right of integrity, are not licensed under this 157 | Public License, nor are publicity, privacy, and/or other similar personality 158 | rights; however, to the extent possible, the Licensor waives and/or agrees 159 | not to assert any such rights held by the Licensor to the limited extent necessary 160 | to allow You to exercise the Licensed Rights, but not otherwise. 161 | 162 | 2. Patent and trademark rights are not licensed under this Public License. 163 | 164 | 3. To the extent possible, the Licensor waives any right to collect royalties 165 | from You for the exercise of the Licensed Rights, whether directly or through 166 | a collecting society under any voluntary or waivable statutory or compulsory 167 | licensing scheme. In all other cases the Licensor expressly reserves any right 168 | to collect such royalties. 169 | 170 | Section 3 – License Conditions. 171 | 172 | Your exercise of the Licensed Rights is expressly made subject to the following 173 | conditions. 174 | 175 | a. Attribution. 176 | 177 | 1. If You Share the Licensed Material (including in modified form), You must: 178 | 179 | A. retain the following if it is supplied by the Licensor with the Licensed 180 | Material: 181 | 182 | i. identification of the creator(s) of the Licensed Material and any others 183 | designated to receive attribution, in any reasonable manner requested by the 184 | Licensor (including by pseudonym if designated); 185 | 186 | ii. a copyright notice; 187 | 188 | iii. a notice that refers to this Public License; 189 | 190 | iv. a notice that refers to the disclaimer of warranties; 191 | 192 | v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; 193 | 194 | B. indicate if You modified the Licensed Material and retain an indication 195 | of any previous modifications; and 196 | 197 | C. indicate the Licensed Material is licensed under this Public License, and 198 | include the text of, or the URI or hyperlink to, this Public License. 199 | 200 | 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner 201 | based on the medium, means, and context in which You Share the Licensed Material. 202 | For example, it may be reasonable to satisfy the conditions by providing a 203 | URI or hyperlink to a resource that includes the required information. 204 | 205 | 3. If requested by the Licensor, You must remove any of the information required 206 | by Section 3(a)(1)(A) to the extent reasonably practicable. 207 | 208 | 4. If You Share Adapted Material You produce, the Adapter's License You apply 209 | must not prevent recipients of the Adapted Material from complying with this 210 | Public License. 211 | 212 | Section 4 – Sui Generis Database Rights. 213 | 214 | Where the Licensed Rights include Sui Generis Database Rights that apply to 215 | Your use of the Licensed Material: 216 | 217 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, 218 | reuse, reproduce, and Share all or a substantial portion of the contents of 219 | the database; 220 | 221 | b. if You include all or a substantial portion of the database contents in 222 | a database in which You have Sui Generis Database Rights, then the database 223 | in which You have Sui Generis Database Rights (but not its individual contents) 224 | is Adapted Material; and 225 | 226 | c. You must comply with the conditions in Section 3(a) if You Share all or 227 | a substantial portion of the contents of the database. 228 | 229 | For the avoidance of doubt, this Section 4 supplements and does not replace 230 | Your obligations under this Public License where the Licensed Rights include 231 | other Copyright and Similar Rights. 232 | 233 | Section 5 – Disclaimer of Warranties and Limitation of Liability. 234 | 235 | a. Unless otherwise separately undertaken by the Licensor, to the extent possible, 236 | the Licensor offers the Licensed Material as-is and as-available, and makes 237 | no representations or warranties of any kind concerning the Licensed Material, 238 | whether express, implied, statutory, or other. This includes, without limitation, 239 | warranties of title, merchantability, fitness for a particular purpose, non-infringement, 240 | absence of latent or other defects, accuracy, or the presence or absence of 241 | errors, whether or not known or discoverable. Where disclaimers of warranties 242 | are not allowed in full or in part, this disclaimer may not apply to You. 243 | 244 | b. To the extent possible, in no event will the Licensor be liable to You 245 | on any legal theory (including, without limitation, negligence) or otherwise 246 | for any direct, special, indirect, incidental, consequential, punitive, exemplary, 247 | or other losses, costs, expenses, or damages arising out of this Public License 248 | or use of the Licensed Material, even if the Licensor has been advised of 249 | the possibility of such losses, costs, expenses, or damages. Where a limitation 250 | of liability is not allowed in full or in part, this limitation may not apply 251 | to You. 252 | 253 | c. The disclaimer of warranties and limitation of liability provided above 254 | shall be interpreted in a manner that, to the extent possible, most closely 255 | approximates an absolute disclaimer and waiver of all liability. 256 | 257 | Section 6 – Term and Termination. 258 | 259 | a. This Public License applies for the term of the Copyright and Similar Rights 260 | licensed here. However, if You fail to comply with this Public License, then 261 | Your rights under this Public License terminate automatically. 262 | 263 | b. Where Your right to use the Licensed Material has terminated under Section 264 | 6(a), it reinstates: 265 | 266 | 1. automatically as of the date the violation is cured, provided it is cured 267 | within 30 days of Your discovery of the violation; or 268 | 269 | 2. upon express reinstatement by the Licensor. 270 | 271 | c. For the avoidance of doubt, this Section 6(b) does not affect any right 272 | the Licensor may have to seek remedies for Your violations of this Public 273 | License. 274 | 275 | d. For the avoidance of doubt, the Licensor may also offer the Licensed Material 276 | under separate terms or conditions or stop distributing the Licensed Material 277 | at any time; however, doing so will not terminate this Public License. 278 | 279 | e. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. 280 | 281 | Section 7 – Other Terms and Conditions. 282 | 283 | a. The Licensor shall not be bound by any additional or different terms or 284 | conditions communicated by You unless expressly agreed. 285 | 286 | b. Any arrangements, understandings, or agreements regarding the Licensed 287 | Material not stated herein are separate from and independent of the terms 288 | and conditions of this Public License. 289 | 290 | Section 8 – Interpretation. 291 | 292 | a. For the avoidance of doubt, this Public License does not, and shall not 293 | be interpreted to, reduce, limit, restrict, or impose conditions on any use 294 | of the Licensed Material that could lawfully be made without permission under 295 | this Public License. 296 | 297 | b. To the extent possible, if any provision of this Public License is deemed 298 | unenforceable, it shall be automatically reformed to the minimum extent necessary 299 | to make it enforceable. If the provision cannot be reformed, it shall be severed 300 | from this Public License without affecting the enforceability of the remaining 301 | terms and conditions. 302 | 303 | c. No term or condition of this Public License will be waived and no failure 304 | to comply consented to unless expressly agreed to by the Licensor. 305 | 306 | d. Nothing in this Public License constitutes or may be interpreted as a limitation 307 | upon, or waiver of, any privileges and immunities that apply to the Licensor 308 | or You, including from the legal processes of any jurisdiction or authority. 309 | 310 | Creative Commons is not a party to its public licenses. Notwithstanding, Creative 311 | Commons may elect to apply one of its public licenses to material it publishes 312 | and in those instances will be considered the "Licensor." The text of the 313 | Creative Commons public licenses is dedicated to the public domain under the 314 | CC0 Public Domain Dedication. Except for the limited purpose of indicating 315 | that material is shared under a Creative Commons public license or as otherwise 316 | permitted by the Creative Commons policies published at creativecommons.org/policies, 317 | Creative Commons does not authorize the use of the trademark "Creative Commons" 318 | or any other trademark or logo of Creative Commons without its prior written 319 | consent including, without limitation, in connection with any unauthorized 320 | modifications to any of its public licenses or any other arrangements, understandings, 321 | or agreements concerning use of licensed material. For the avoidance of doubt, 322 | this paragraph does not form part of the public licenses. 323 | 324 | Creative Commons may be contacted at creativecommons.org. 325 | --------------------------------------------------------------------------------