mido-1.3.3/0000755000175100001770000000000014706731615013265 5ustar runnerdocker00000000000000mido-1.3.3/.github/0000755000175100001770000000000014706731615014625 5ustar runnerdocker00000000000000mido-1.3.3/.github/ISSUE_TEMPLATE/0000755000175100001770000000000014706731615017010 5ustar runnerdocker00000000000000mido-1.3.3/.github/ISSUE_TEMPLATE/bug_report.md0000644000175100001770000000162514706731605021505 0ustar runnerdocker00000000000000--- name: Bug report about: Create a report to help us improve title: '' labels: bug assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. do this 2. do that 3. … **Expected behavior** A clear and concise description of what you expected to happen. **Code Snippet** If applicable, add a code snippet to help explain your problem. ```python # Code here ``` **Context (please complete the following information):** - Operating system name & version: [e.g. Ubuntu 23.04] - Python version: [e.g. Python 3.11.4] - mido package version (Retrievable using `mido.version_info`) & installation method (Distribution repository, PyPI, source…): [e.g. installed from PyPI] - backend used (amidi, portmidi, rtmidi, PyGame…): python-rtmidi **Additional context** Add any other context about the problem here. mido-1.3.3/.github/ISSUE_TEMPLATE/bug_report.md.license0000644000175100001770000000015314706731605023121 0ustar runnerdocker00000000000000SPDX-FileCopyrightText: 2023 Raphaël Doursenaud SPDX-License-Identifier: CC0-1.0 mido-1.3.3/.github/ISSUE_TEMPLATE/feature_request.md0000644000175100001770000000114314706731605022533 0ustar runnerdocker00000000000000--- name: Feature request about: Suggest an enhancement for this project title: '' labels: enhancement assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. mido-1.3.3/.github/ISSUE_TEMPLATE/feature_request.md.license0000644000175100001770000000015314706731605024154 0ustar runnerdocker00000000000000SPDX-FileCopyrightText: 2023 Raphaël Doursenaud SPDX-License-Identifier: CC0-1.0 mido-1.3.3/.github/dependabot.yml0000644000175100001770000000121514706731605017453 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2023 Raphaël Doursenaud # # SPDX-License-Identifier: CC0-1.0 # To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "pip" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "monthly" assignees: - "rdoursenaud" mido-1.3.3/.github/workflows/0000755000175100001770000000000014706731615016662 5ustar runnerdocker00000000000000mido-1.3.3/.github/workflows/documentation.yml0000644000175100001770000000205114706731605022253 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2023 Raphaël Doursenaud # # SPDX-License-Identifier: CC0-1.0 name: Documentation on: pull_request: branches: - main push: branches: - main workflow_dispatch: permissions: contents: write jobs: build-github-pages: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version-file: 'pyproject.toml' cache: 'pip' - name: Upgrade pip run: python3 -m pip install --upgrade pip setuptools wheel - name: Install mido run: python3 -m pip install --quiet .[build-docs] - name: Sphinx build run: sphinx-build -j auto docs docs/_build - name: Deploy uses: peaceiris/actions-gh-pages@v3 if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} with: publish_branch: gh-pages github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: docs/_build/ force_orphan: true mido-1.3.3/.github/workflows/release.yml0000644000175100001770000000555214706731605021033 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2023 Raphaël Doursenaud # # SPDX-License-Identifier: CC0-1.0 # This workflow needs access to 2 GitHub secrets: # - TEST_PYPI_TOKEN: The API token to deploy on https://test.pypi.org # - PROD_PYPI_TOKEN: The API token to deploy on https://pypi.org # # Both API tokens shall be generated from a user account with appropriate # permissions on the target project and have their scope set to the project. # # The API tokens have then to be registered as secrets in the GitHub # repository's configuration under the names specified above. # See: https://docs.github.com/actions/security-guides/encrypted-secrets # TODO: Allow the publishing of pre-releases (a, b, rc). # The "Test installation" step requires a `--pre` argument to install # pre-releases. name: Release on: push: tags: - "*" workflow_dispatch: jobs: build: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: python-version-file: 'pyproject.toml' cache: 'pip' - name: Upgrade pip run: python3 -m pip install --upgrade pip setuptools wheel - name: Install or upgrade build run: python3 -m pip install --upgrade build # Build dependencies are automatically installed from `pyproject.toml`. - name: Build mido run: python3 -m build # Store build artifacts - uses: actions/upload-artifact@v3 with: name: mido-build path: dist/ publish-test: runs-on: ubuntu-latest needs: build steps: # Retrieve build artifacts - uses: actions/download-artifact@v3 with: name: mido-build path: dist/ - name: Install twine run: python3 -m pip install --upgrade twine - name: Check distribution name run: twine check dist/* - name: Publish to test.pypi.org run: twine upload --repository testpypi dist/* env: TWINE_NON_INTERACTIVE: 1 TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.TEST_PYPI_TOKEN }} - name: Test installation run: | python3 -m pip install --index-url https://test.pypi.org/simple/ --no-deps mido - name: Test importing package run: python3 -c "import mido; print(mido.version_info)" publish-release: runs-on: ubuntu-latest needs: - build - publish-test steps: - uses: actions/download-artifact@v3 with: name: mido-build path: dist/ - name: Install twine run: python3 -m pip install --upgrade twine - name: Publish to pypi.org run: twine upload dist/* env: TWINE_NON_INTERACTIVE: 1 TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PROD_PYPI_TOKEN }} mido-1.3.3/.github/workflows/test.yml0000644000175100001770000000672314706731605020373 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2020 Ole Martin Bjorndalen # # SPDX-License-Identifier: CC0-1.0 name: Tests on: pull_request: branches: - main push: branches-ignore: - gh-pages workflow_dispatch: jobs: style: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: python-version-file: 'pyproject.toml' cache: 'pip' - name: Upgrade pip run: python3 -m pip install --upgrade pip setuptools wheel - name: Install mido in dev mode run: python3 -m pip install --quiet .[lint-code] - name: Lint code with ruff run: ruff check . env: RUFF_OUTPUT_FORMAT: github reuse: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: python-version-file: 'pyproject.toml' cache: 'pip' - name: Upgrade pip run: python3 -m pip install --upgrade pip setuptools wheel - name: Install mido in dev mode run: python3 -m pip install --quiet .[lint-reuse] - name: Lint reuse run: reuse lint manifest: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: python-version-file: 'pyproject.toml' cache: 'pip' - name: Upgrade pip run: python3 -m pip install --upgrade pip setuptools wheel - name: Install mido in dev mode run: python3 -m pip install --quiet .[check-manifest] - name: Check manifest is complete run: check-manifest --verbose docs: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: python-version-file: 'pyproject.toml' cache: 'pip' - name: Upgrade pip run: python3 -m pip install --upgrade pip setuptools wheel - name: Install mido in dev mode run: python3 -m pip install --quiet .[build-docs] - name: Check documentation run: sphinx-build -b linkcheck -j auto -q -W -E --keep-going docs docs/_build test: needs: - docs - manifest - reuse - style runs-on: ${{ matrix.os }} strategy: matrix: os: - macOS-latest - ubuntu-latest - windows-latest python-version: - '3.7' - '3.8' - '3.9' - '3.10' - '3.11' - '3.12' - '3.13' exclude: # Python < v3.8 does not support Apple Silicon ARM64. - python-version: "3.7" os: macOS-latest include: # So run those legacy versions on Intel CPUs. - python-version: "3.7" os: macOS-13 steps: - name: Checkout uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} cache: 'pip' - name: Upgrade pip run: python3 -m pip install --upgrade pip setuptools wheel - name: Install mido in dev mode run: python3 -m pip install --quiet .[test-code] - name: Test with pytest run: pytest mido-1.3.3/.gitignore0000644000175100001770000000051614706731605015256 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen # # SPDX-License-Identifier: CC0-1.0 __pycache__ *.pyc *.pyo *~ /.vscode/ /.idea/ .cache /docs/_build/ /dist/ /build/ /*.egg-info/ .pytest_cache # Version information is generated by setuptools-scm mido/_version.py # Documentation built by Sphinx docs/_build mido-1.3.3/.readthedocs.yaml0000644000175100001770000000105714706731605016516 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2023 Raphaël Doursenaud # # SPDX-License-Identifier: CC0-1.0 # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details version: 2 # Required build: os: ubuntu-22.04 tools: python: "3" # Always use the latest CPython version python: install: - method: pip path: . extra_requirements: - build-docs sphinx: builder: html configuration: docs/conf.py fail_on_warning: false formats: - epub - htmlzip - pdf mido-1.3.3/.reuse/0000755000175100001770000000000014706731615014466 5ustar runnerdocker00000000000000mido-1.3.3/.reuse/dep50000644000175100001770000000045014706731605015244 0ustar runnerdocker00000000000000Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: mido Upstream-Contact: Ole Martin Bjorndalen Source: https://mido.readthedocs.io # Sample paragraph, commented out: # # Files: src/* # Copyright: $YEAR $NAME <$CONTACT> # License: ... mido-1.3.3/LICENSE0000644000175100001770000000206614706731605014275 0ustar runnerdocker00000000000000The MIT License Copyright (c) Ole Martin Bjørndalen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. mido-1.3.3/LICENSES/0000755000175100001770000000000014706731615014472 5ustar runnerdocker00000000000000mido-1.3.3/LICENSES/CC-BY-4.0.txt0000644000175100001770000004117714706731605016340 0ustar runnerdocker00000000000000Creative Commons Attribution 4.0 International Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. Using Creative Commons Public Licenses Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. Considerations for licensors: Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. More considerations for licensors. Considerations for the public: By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor’s permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. More considerations for the public. Creative Commons Attribution 4.0 International Public License By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. Section 1 – Definitions. a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. b. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. c. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. d. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. e. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. f. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License. g. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. h. Licensor means the individual(s) or entity(ies) granting rights under this Public License. i. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. j. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. k. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. Section 2 – Scope. a. License grant. 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: A. reproduce and Share the Licensed Material, in whole or in part; and B. produce, reproduce, and Share Adapted Material. 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. 3. Term. The term of this Public License is specified in Section 6(a). 4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. 5. Downstream recipients. A. Offer from the Licensor – Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. B. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. 6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). b. Other rights. 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. 2. Patent and trademark rights are not licensed under this Public License. 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties. Section 3 – License Conditions. Your exercise of the Licensed Rights is expressly made subject to the following conditions. a. Attribution. 1. If You Share the Licensed Material (including in modified form), You must: A. retain the following if it is supplied by the Licensor with the Licensed Material: i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); ii. a copyright notice; iii. a notice that refers to this Public License; iv. a notice that refers to the disclaimer of warranties; v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. 4. If You Share Adapted Material You produce, the Adapter's License You apply must not prevent recipients of the Adapted Material from complying with this Public License. Section 4 – Sui Generis Database Rights. Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database; b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. Section 5 – Disclaimer of Warranties and Limitation of Liability. a. Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You. b. To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You. c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. Section 6 – Term and Termination. a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or 2. upon express reinstatement by the Licensor. c. For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. d. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. e. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. Section 7 – Other Terms and Conditions. a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. Section 8 – Interpretation. a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. Creative Commons may be contacted at creativecommons.org. mido-1.3.3/LICENSES/CC-BY-SA-3.0.txt0000644000175100001770000005147114706731605016636 0ustar runnerdocker00000000000000Creative Commons Attribution-ShareAlike 3.0 Unported CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM ITS USE. License THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED. BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND CONDITIONS. 1. Definitions a. "Adaptation" means a work based upon the Work, or upon the Work and other pre-existing works, such as a translation, adaptation, derivative work, arrangement of music or other alterations of a literary or artistic work, or phonogram or performance and includes cinematographic adaptations or any other form in which the Work may be recast, transformed, or adapted including in any form recognizably derived from the original, except that a work that constitutes a Collection will not be considered an Adaptation for the purpose of this License. For the avoidance of doubt, where the Work is a musical work, performance or phonogram, the synchronization of the Work in timed-relation with a moving image ("synching") will be considered an Adaptation for the purpose of this License. b. "Collection" means a collection of literary or artistic works, such as encyclopedias and anthologies, or performances, phonograms or broadcasts, or other works or subject matter other than works listed in Section 1(f) below, which, by reason of the selection and arrangement of their contents, constitute intellectual creations, in which the Work is included in its entirety in unmodified form along with one or more other contributions, each constituting separate and independent works in themselves, which together are assembled into a collective whole. A work that constitutes a Collection will not be considered an Adaptation (as defined below) for the purposes of this License. c. "Creative Commons Compatible License" means a license that is listed at http://creativecommons.org/compatiblelicenses that has been approved by Creative Commons as being essentially equivalent to this License, including, at a minimum, because that license: (i) contains terms that have the same purpose, meaning and effect as the License Elements of this License; and, (ii) explicitly permits the relicensing of adaptations of works made available under that license under this License or a Creative Commons jurisdiction license with the same License Elements as this License. d. "Distribute" means to make available to the public the original and copies of the Work or Adaptation, as appropriate, through sale or other transfer of ownership. e. "License Elements" means the following high-level license attributes as selected by Licensor and indicated in the title of this License: Attribution, ShareAlike. f. "Licensor" means the individual, individuals, entity or entities that offer(s) the Work under the terms of this License. g. "Original Author" means, in the case of a literary or artistic work, the individual, individuals, entity or entities who created the Work or if no individual or entity can be identified, the publisher; and in addition (i) in the case of a performance the actors, singers, musicians, dancers, and other persons who act, sing, deliver, declaim, play in, interpret or otherwise perform literary or artistic works or expressions of folklore; (ii) in the case of a phonogram the producer being the person or legal entity who first fixes the sounds of a performance or other sounds; and, (iii) in the case of broadcasts, the organization that transmits the broadcast. h. "Work" means the literary and/or artistic work offered under the terms of this License including without limitation any production in the literary, scientific and artistic domain, whatever may be the mode or form of its expression including digital form, such as a book, pamphlet and other writing; a lecture, address, sermon or other work of the same nature; a dramatic or dramatico-musical work; a choreographic work or entertainment in dumb show; a musical composition with or without words; a cinematographic work to which are assimilated works expressed by a process analogous to cinematography; a work of drawing, painting, architecture, sculpture, engraving or lithography; a photographic work to which are assimilated works expressed by a process analogous to photography; a work of applied art; an illustration, map, plan, sketch or three-dimensional work relative to geography, topography, architecture or science; a performance; a broadcast; a phonogram; a compilation of data to the extent it is protected as a copyrightable work; or a work performed by a variety or circus performer to the extent it is not otherwise considered a literary or artistic work. i. "You" means an individual or entity exercising rights under this License who has not previously violated the terms of this License with respect to the Work, or who has received express permission from the Licensor to exercise rights under this License despite a previous violation. j. "Publicly Perform" means to perform public recitations of the Work and to communicate to the public those public recitations, by any means or process, including by wire or wireless means or public digital performances; to make available to the public Works in such a way that members of the public may access these Works from a place and at a place individually chosen by them; to perform the Work to the public by any means or process and the communication to the public of the performances of the Work, including by public digital performance; to broadcast and rebroadcast the Work by any means including signs, sounds or images. k. "Reproduce" means to make copies of the Work by any means including without limitation by sound or visual recordings and the right of fixation and reproducing fixations of the Work, including storage of a protected performance or phonogram in digital form or other electronic medium. 2. Fair Dealing Rights. Nothing in this License is intended to reduce, limit, or restrict any uses free from copyright or rights arising from limitations or exceptions that are provided for in connection with the copyright protection under copyright law or other applicable laws. 3. License Grant. Subject to the terms and conditions of this License, Licensor hereby grants You a worldwide, royalty-free, non-exclusive, perpetual (for the duration of the applicable copyright) license to exercise the rights in the Work as stated below: a. to Reproduce the Work, to incorporate the Work into one or more Collections, and to Reproduce the Work as incorporated in the Collections; b. to create and Reproduce Adaptations provided that any such Adaptation, including any translation in any medium, takes reasonable steps to clearly label, demarcate or otherwise identify that changes were made to the original Work. For example, a translation could be marked "The original work was translated from English to Spanish," or a modification could indicate "The original work has been modified."; c. to Distribute and Publicly Perform the Work including as incorporated in Collections; and, d. to Distribute and Publicly Perform Adaptations. e. For the avoidance of doubt: i. Non-waivable Compulsory License Schemes. In those jurisdictions in which the right to collect royalties through any statutory or compulsory licensing scheme cannot be waived, the Licensor reserves the exclusive right to collect such royalties for any exercise by You of the rights granted under this License; ii. Waivable Compulsory License Schemes. In those jurisdictions in which the right to collect royalties through any statutory or compulsory licensing scheme can be waived, the Licensor waives the exclusive right to collect such royalties for any exercise by You of the rights granted under this License; and, iii. Voluntary License Schemes. The Licensor waives the right to collect royalties, whether individually or, in the event that the Licensor is a member of a collecting society that administers voluntary licensing schemes, via that society, from any exercise by You of the rights granted under this License. The above rights may be exercised in all media and formats whether now known or hereafter devised. The above rights include the right to make such modifications as are technically necessary to exercise the rights in other media and formats. Subject to Section 8(f), all rights not expressly granted by Licensor are hereby reserved. 4. Restrictions. The license granted in Section 3 above is expressly made subject to and limited by the following restrictions: a. You may Distribute or Publicly Perform the Work only under the terms of this License. You must include a copy of, or the Uniform Resource Identifier (URI) for, this License with every copy of the Work You Distribute or Publicly Perform. You may not offer or impose any terms on the Work that restrict the terms of this License or the ability of the recipient of the Work to exercise the rights granted to that recipient under the terms of the License. You may not sublicense the Work. You must keep intact all notices that refer to this License and to the disclaimer of warranties with every copy of the Work You Distribute or Publicly Perform. When You Distribute or Publicly Perform the Work, You may not impose any effective technological measures on the Work that restrict the ability of a recipient of the Work from You to exercise the rights granted to that recipient under the terms of the License. This Section 4(a) applies to the Work as incorporated in a Collection, but this does not require the Collection apart from the Work itself to be made subject to the terms of this License. If You create a Collection, upon notice from any Licensor You must, to the extent practicable, remove from the Collection any credit as required by Section 4(c), as requested. If You create an Adaptation, upon notice from any Licensor You must, to the extent practicable, remove from the Adaptation any credit as required by Section 4(c), as requested. b. You may Distribute or Publicly Perform an Adaptation only under the terms of: (i) this License; (ii) a later version of this License with the same License Elements as this License; (iii) a Creative Commons jurisdiction license (either this or a later license version) that contains the same License Elements as this License (e.g., Attribution-ShareAlike 3.0 US)); (iv) a Creative Commons Compatible License. If you license the Adaptation under one of the licenses mentioned in (iv), you must comply with the terms of that license. If you license the Adaptation under the terms of any of the licenses mentioned in (i), (ii) or (iii) (the "Applicable License"), you must comply with the terms of the Applicable License generally and the following provisions: (I) You must include a copy of, or the URI for, the Applicable License with every copy of each Adaptation You Distribute or Publicly Perform; (II) You may not offer or impose any terms on the Adaptation that restrict the terms of the Applicable License or the ability of the recipient of the Adaptation to exercise the rights granted to that recipient under the terms of the Applicable License; (III) You must keep intact all notices that refer to the Applicable License and to the disclaimer of warranties with every copy of the Work as included in the Adaptation You Distribute or Publicly Perform; (IV) when You Distribute or Publicly Perform the Adaptation, You may not impose any effective technological measures on the Adaptation that restrict the ability of a recipient of the Adaptation from You to exercise the rights granted to that recipient under the terms of the Applicable License. This Section 4(b) applies to the Adaptation as incorporated in a Collection, but this does not require the Collection apart from the Adaptation itself to be made subject to the terms of the Applicable License. c. If You Distribute, or Publicly Perform the Work or any Adaptations or Collections, You must, unless a request has been made pursuant to Section 4(a), keep intact all copyright notices for the Work and provide, reasonable to the medium or means You are utilizing: (i) the name of the Original Author (or pseudonym, if applicable) if supplied, and/or if the Original Author and/or Licensor designate another party or parties (e.g., a sponsor institute, publishing entity, journal) for attribution ("Attribution Parties") in Licensor's copyright notice, terms of service or by other reasonable means, the name of such party or parties; (ii) the title of the Work if supplied; (iii) to the extent reasonably practicable, the URI, if any, that Licensor specifies to be associated with the Work, unless such URI does not refer to the copyright notice or licensing information for the Work; and (iv) , consistent with Ssection 3(b), in the case of an Adaptation, a credit identifying the use of the Work in the Adaptation (e.g., "French translation of the Work by Original Author," or "Screenplay based on original Work by Original Author"). The credit required by this Section 4(c) may be implemented in any reasonable manner; provided, however, that in the case of a Adaptation or Collection, at a minimum such credit will appear, if a credit for all contributing authors of the Adaptation or Collection appears, then as part of these credits and in a manner at least as prominent as the credits for the other contributing authors. For the avoidance of doubt, You may only use the credit required by this Section for the purpose of attribution in the manner set out above and, by exercising Your rights under this License, You may not implicitly or explicitly assert or imply any connection with, sponsorship or endorsement by the Original Author, Licensor and/or Attribution Parties, as appropriate, of You or Your use of the Work, without the separate, express prior written permission of the Original Author, Licensor and/or Attribution Parties. d. Except as otherwise agreed in writing by the Licensor or as may be otherwise permitted by applicable law, if You Reproduce, Distribute or Publicly Perform the Work either by itself or as part of any Adaptations or Collections, You must not distort, mutilate, modify or take other derogatory action in relation to the Work which would be prejudicial to the Original Author's honor or reputation. Licensor agrees that in those jurisdictions (e.g. Japan), in which any exercise of the right granted in Section 3(b) of this License (the right to make Adaptations) would be deemed to be a distortion, mutilation, modification or other derogatory action prejudicial to the Original Author's honor and reputation, the Licensor will waive or not assert, as appropriate, this Section, to the fullest extent permitted by the applicable national law, to enable You to reasonably exercise Your right under Section 3(b) of this License (right to make Adaptations) but not otherwise. 5. Representations, Warranties and Disclaimer UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY, FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU. 6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 7. Termination a. This License and the rights granted hereunder will terminate automatically upon any breach by You of the terms of this License. Individuals or entities who have received Adaptations or Collections from You under this License, however, will not have their licenses terminated provided such individuals or entities remain in full compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will survive any termination of this License. b. Subject to the above terms and conditions, the license granted here is perpetual (for the duration of the applicable copyright in the Work). Notwithstanding the above, Licensor reserves the right to release the Work under different license terms or to stop distributing the Work at any time; provided, however that any such election will not serve to withdraw this License (or any other license that has been, or is required to be, granted under the terms of this License), and this License will continue in full force and effect unless terminated as stated above. 8. Miscellaneous a. Each time You Distribute or Publicly Perform the Work or a Collection, the Licensor offers to the recipient a license to the Work on the same terms and conditions as the license granted to You under this License. b. Each time You Distribute or Publicly Perform an Adaptation, Licensor offers to the recipient a license to the original Work on the same terms and conditions as the license granted to You under this License. c. If any provision of this License is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this License, and without further action by the parties to this agreement, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. d. No term or provision of this License shall be deemed waived and no breach consented to unless such waiver or consent shall be in writing and signed by the party to be charged with such waiver or consent. e. This License constitutes the entire agreement between the parties with respect to the Work licensed here. There are no understandings, agreements or representations with respect to the Work not specified here. Licensor shall not be bound by any additional provisions that may appear in any communication from You. This License may not be modified without the mutual written agreement of the Licensor and You. f. The rights granted under, and the subject matter referenced, in this License were drafted utilizing the terminology of the Berne Convention for the Protection of Literary and Artistic Works (as amended on September 28, 1979), the Rome Convention of 1961, the WIPO Copyright Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996 and the Universal Copyright Convention (as revised on July 24, 1971). These rights and subject matter take effect in the relevant jurisdiction in which the License terms are sought to be enforced according to the corresponding provisions of the implementation of those treaty provisions in the applicable national law. If the standard suite of rights granted under applicable copyright law includes additional rights not granted under this License, such additional rights are deemed to be included in the License; this License is not intended to restrict the license of any rights under applicable law. Creative Commons Notice Creative Commons is not a party to this License, and makes no warranty whatsoever in connection with the Work. Creative Commons will not be liable to You or any party on any legal theory for any damages whatsoever, including without limitation any general, special, incidental or consequential damages arising in connection to this license. Notwithstanding the foregoing two (2) sentences, if Creative Commons has expressly identified itself as the Licensor hereunder, it shall have all rights and obligations of Licensor. Except for the limited purpose of indicating to the public that the Work is licensed under the CCPL, Creative Commons does not authorize the use by either party of the trademark "Creative Commons" or any related trademark or logo of Creative Commons without the prior written consent of Creative Commons. Any permitted use will be in compliance with Creative Commons' then-current trademark usage guidelines, as may be published on its website or otherwise made available upon request from time to time. For the avoidance of doubt, this trademark restriction does not form part of the License. Creative Commons may be contacted at http://creativecommons.org/. mido-1.3.3/LICENSES/CC0-1.0.txt0000644000175100001770000001561014706731605016076 0ustar runnerdocker00000000000000Creative Commons Legal Code CC0 1.0 Universal CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER. Statement of Purpose The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others. For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. 1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: i. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; ii. moral rights retained by the original author(s) and/or performer(s); iii. publicity and privacy rights pertaining to a person's image or likeness depicted in a Work; iv. rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below; v. rights protecting the extraction, dissemination, use and reuse of data in a Work; vi. database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and vii. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. 2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. 3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. 4. Limitations and Disclaimers. a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. b. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. c. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. mido-1.3.3/LICENSES/MIT.txt0000644000175100001770000000206614706731605015667 0ustar runnerdocker00000000000000MIT License Copyright (c) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. mido-1.3.3/MANIFEST.in0000644000175100001770000000126114706731605015022 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen # # SPDX-License-Identifier: CC0-1.0 exclude SECURITY.md prune docs/_build prune logo include README.rst LICENSE .readthedocs.yaml .reuse/dep5 include docs/_static/.gitkeep recursive-include docs *.bat recursive-include docs *.py recursive-include docs *.rst recursive-include docs Makefile recursive-include docs/images *.svg recursive-include docs/images *.license recursive-include examples *.py recursive-include examples *.py recursive-include examples *.sh recursive-include extras *.py recursive-include extras *.rst recursive-include LICENSES *.txt recursive-include mido *.py recursive-include tests *.py mido-1.3.3/PKG-INFO0000644000175100001770000001227014706731615014364 0ustar runnerdocker00000000000000Metadata-Version: 2.1 Name: mido Version: 1.3.3 Summary: MIDI Objects for Python Author-email: Ole Martin Bjorndalen Maintainer-email: Radovan Bast , Raphaël Doursenaud License: MIT Project-URL: documentation, https://mido.readthedocs.io Project-URL: source, https://github.com/mido/mido Keywords: python,midi,midifile Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Natural Language :: English Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Topic :: Multimedia :: Sound/Audio :: MIDI Classifier: Topic :: Software Development Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Utilities Requires-Python: ~=3.7 Description-Content-Type: text/x-rst Provides-Extra: build-docs Provides-Extra: check-manifest Provides-Extra: lint-code Provides-Extra: lint-reuse Provides-Extra: ports-pygame Provides-Extra: ports-rtmidi Provides-Extra: ports-rtmidi-python Provides-Extra: release Provides-Extra: test-code Provides-Extra: dev Provides-Extra: ports-all License-File: LICENSE .. SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen .. .. SPDX-License-Identifier: CC-BY-4.0 Mido - MIDI Objects for Python ============================== .. image:: https://img.shields.io/badge/License-MIT-blue.svg :alt: MIT License :target: https://github.com/mido/mido/blob/main/LICENSES/MIT.txt .. image:: https://img.shields.io/pypi/v/mido.svg :alt: PyPi version :target: https://pypi.org/project/mido .. image:: https://img.shields.io/pypi/pyversions/mido.svg :alt: Python version :target: https://python.org .. image:: https://pepy.tech/badge/mido :alt: Downloads :target: https://pepy.tech/project/mido .. image:: https://github.com/mido/mido/workflows/Test/badge.svg :alt: Test status :target: https://github.com/mido/mido/actions .. image:: https://readthedocs.org/projects/mido/badge/?version=latest :alt: Docs status :target: https://mido.readthedocs.io/ .. image:: https://api.reuse.software/badge/github.com/mido/mido :alt: REUSE status :target: https://api.reuse.software/info/github.com/mido/mido .. image:: https://www.bestpractices.dev/projects/7987/badge :alt: OpenSSF Best Practices :target: https://www.bestpractices.dev/projects/7987 Mido is a library for working with MIDI messages and ports: .. code-block:: python >>> import mido >>> msg = mido.Message('note_on', note=60) >>> msg.type 'note_on' >>> msg.note 60 >>> msg.bytes() [144, 60, 64] >>> msg.copy(channel=2) Message('note_on', channel=2, note=60, velocity=64, time=0) .. code-block:: python port = mido.open_output('Port Name') port.send(msg) .. code-block:: python with mido.open_input() as inport: for msg in inport: print(msg) .. code-block:: python mid = mido.MidiFile('song.mid') for msg in mid.play(): port.send(msg) Full documentation at https://mido.readthedocs.io/ Main Features ------------- * convenient message objects. * supports RtMidi, PortMidi and Pygame. New backends are easy to write. * full support for all 18 messages defined by the MIDI standard. * standard port API allows all kinds of input and output ports to be used interchangeably. New port types can be written by subclassing and overriding a few methods. * includes a reusable MIDI stream parser. * full support for MIDI files (read, write, create and play) with complete access to every message in the file, including all common meta messages. * can read and write SYX files (binary and plain text). * implements (somewhat experimental) MIDI over TCP/IP with socket ports. This allows for example wireless MIDI between two computers. * includes programs for playing MIDI files, listing ports and serving and forwarding ports over a network. Status ------ 1.3 is the fourth stable release. This project uses `Semantic Versioning `_. Requirements ------------ Mido requires Python 3.7 or higher. Installing ---------- :: python3 -m pip install mido Or, alternatively, if you want to use ports with the default backend:: python3 -m pip install mido[ports-rtmidi] See ``docs/backends/`` for other backends. Source Code ----------- https://github.com/mido/mido/ License ------- Mido is released under the terms of the `MIT license `_. Questions and suggestions ------------------------- For questions and proposals which may not fit into issues or pull requests, we recommend to ask and discuss in the `Discussions `_ section. mido-1.3.3/README.rst0000644000175100001770000000705714706731605014764 0ustar runnerdocker00000000000000.. SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen .. .. SPDX-License-Identifier: CC-BY-4.0 Mido - MIDI Objects for Python ============================== .. image:: https://img.shields.io/badge/License-MIT-blue.svg :alt: MIT License :target: https://github.com/mido/mido/blob/main/LICENSES/MIT.txt .. image:: https://img.shields.io/pypi/v/mido.svg :alt: PyPi version :target: https://pypi.org/project/mido .. image:: https://img.shields.io/pypi/pyversions/mido.svg :alt: Python version :target: https://python.org .. image:: https://pepy.tech/badge/mido :alt: Downloads :target: https://pepy.tech/project/mido .. image:: https://github.com/mido/mido/workflows/Test/badge.svg :alt: Test status :target: https://github.com/mido/mido/actions .. image:: https://readthedocs.org/projects/mido/badge/?version=latest :alt: Docs status :target: https://mido.readthedocs.io/ .. image:: https://api.reuse.software/badge/github.com/mido/mido :alt: REUSE status :target: https://api.reuse.software/info/github.com/mido/mido .. image:: https://www.bestpractices.dev/projects/7987/badge :alt: OpenSSF Best Practices :target: https://www.bestpractices.dev/projects/7987 Mido is a library for working with MIDI messages and ports: .. code-block:: python >>> import mido >>> msg = mido.Message('note_on', note=60) >>> msg.type 'note_on' >>> msg.note 60 >>> msg.bytes() [144, 60, 64] >>> msg.copy(channel=2) Message('note_on', channel=2, note=60, velocity=64, time=0) .. code-block:: python port = mido.open_output('Port Name') port.send(msg) .. code-block:: python with mido.open_input() as inport: for msg in inport: print(msg) .. code-block:: python mid = mido.MidiFile('song.mid') for msg in mid.play(): port.send(msg) Full documentation at https://mido.readthedocs.io/ Main Features ------------- * convenient message objects. * supports RtMidi, PortMidi and Pygame. New backends are easy to write. * full support for all 18 messages defined by the MIDI standard. * standard port API allows all kinds of input and output ports to be used interchangeably. New port types can be written by subclassing and overriding a few methods. * includes a reusable MIDI stream parser. * full support for MIDI files (read, write, create and play) with complete access to every message in the file, including all common meta messages. * can read and write SYX files (binary and plain text). * implements (somewhat experimental) MIDI over TCP/IP with socket ports. This allows for example wireless MIDI between two computers. * includes programs for playing MIDI files, listing ports and serving and forwarding ports over a network. Status ------ 1.3 is the fourth stable release. This project uses `Semantic Versioning `_. Requirements ------------ Mido requires Python 3.7 or higher. Installing ---------- :: python3 -m pip install mido Or, alternatively, if you want to use ports with the default backend:: python3 -m pip install mido[ports-rtmidi] See ``docs/backends/`` for other backends. Source Code ----------- https://github.com/mido/mido/ License ------- Mido is released under the terms of the `MIT license `_. Questions and suggestions ------------------------- For questions and proposals which may not fit into issues or pull requests, we recommend to ask and discuss in the `Discussions `_ section. mido-1.3.3/docs/0000755000175100001770000000000014706731615014215 5ustar runnerdocker00000000000000mido-1.3.3/docs/CODE_OF_CONDUCT.rst0000644000175100001770000001366514706731605017236 0ustar runnerdocker00000000000000.. SPDX-FileCopyrightText: 2023 Mido project .. .. SPDX-License-Identifier: CC-BY-SA-3.0 .. include:: shared/licenses_logos.rst Code of Conduct =============== Our Community ------------- Members of the Mido community are **open, considerate, and respectful**. Behaviours that reinforce these values contribute to a positive environment, and include: - **Being open.** Members of the community are open to collaboration, whether it’s on patches, problems, or otherwise. - **Focusing on what is best for the community.** We’re respectful of the processes set forth in the community, and we work within them. - **Acknowledging time and effort.** We’re respectful of the volunteer efforts that permeate the Mido community. We’re thoughtful when addressing the efforts of others, keeping in mind that often times the labor was completed simply for the good of the community. - **Being respectful of differing viewpoints and experiences.** We’re receptive to constructive comments and criticism, as the experiences and skill sets of other members contribute to the whole of our efforts. - **Showing empathy towards other community members.** We’re attentive in our communications, whether in person or online, and we’re tactful when approaching differing views. - **Being considerate.** Members of the community are considerate of their peers – other Mido users. - **Being respectful.** We’re respectful of others, their positions, their skills, their commitments, and their efforts. - **Gracefully accepting constructive criticism.** When we disagree, we are courteous in raising our issues. - **Using welcoming and inclusive language.** We’re accepting of all who wish to take part in our activities, fostering an environment where anyone can participate and everyone can make a difference. Our Standards ------------- Every member of our community has the right to have their identity respected. The Mido community is dedicated to providing a positive experience for everyone, regardless of age, gender identity and expression, sexual orientation, disability, physical appearance, body size, ethnicity, nationality, race, or religion (or lack thereof), education, or socio-economic status. **Examples of unacceptable behavior** by participants include: - Harassment of any participants in any form - Deliberate intimidation, stalking, or following - Logging or taking screenshots of online activity for harassment purposes - Publishing others’ private information, such as a physical or electronic address, without explicit permission - Violent threats or language directed against another person - Incitement of violence or harassment towards any individual, including encouraging a person to commit suicide or to engage in self-harm - Creating additional online accounts in order to harass another person or circumvent a ban - Sexual language and imagery in online communities or in any conference venue, including talks - Insults, put downs, or jokes that are based upon stereotypes, that are exclusionary, or that hold others up for ridicule - Excessive swearing - Unwelcome sexual attention or advances - Unwelcome physical contact, including simulated physical contact (eg, textual descriptions like “hug” or “backrub”) without consent or after a request to stop - Pattern of inappropriate social contact, such as requesting/assuming inappropriate levels of intimacy with others - Sustained disruption of online community discussions, in-person presentations, or other in-person events - Continued one-on-one communication after requests to cease - Other conduct that is inappropriate for a professional audience including people of many different backgrounds Community members asked to stop any inappropriate behavior are expected to comply immediately. Consequences ------------ If a participant engages in behavior that violates this code of conduct, the Mido project maintainers may take any action they deem appropriate, including warning the offender or expulsion from the community. Thank you for helping make this a welcoming, friendly community for everyone. Scope ----- This Code of Conduct applies to the following online spaces: - Code repositories, issue trackers, and pull requests made against the Mido GitHub organization. - Discussions in the Mido GitHub organization. This Code of Conduct applies to the following people in the Mido project online spaces: - admins of the online space - maintainers - reviewers - contributors - all community members Contact and Procedure for Handling Incidents -------------------------------------------- Please mention "``@mido``" in the issue or discussion or open a new issue on https://github.com/mido/mido and tag the organization admins using "``@mido``". You can also contact one or several organization admins directly: - radovan.bast@uit.no - ombdalen@gmail.com - raphael@doursenaud.fr We will then immediately discuss with the problematic user and convey why their behavior was inappropriate. We will also explain what the user can do to improve their behavior in the future. We will also explain that if the user continues to behave inappropriately, they will be banned from the community. Depending on the severity of the violation, we may also ban the user immediately. License ------- This Code of Conduct is licensed under the `Creative Commons Attribution-ShareAlike 3.0 Unported License `__. |Creative Commons BY-SA-3.0 License| Attributions ------------ This Code of Conduct was adapted from the `PSF Code of Conduct `__, which was forked from the example policy from the `Geek Feminism wiki, created by the Ada Initiative and other volunteers `__, which is under a `Creative Commons Zero license `__. mido-1.3.3/docs/Makefile0000644000175100001770000001304414706731605015656 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen # # SPDX-License-Identifier: CC0-1.0 # Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Mido.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Mido.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/Mido" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Mido" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." mido-1.3.3/docs/_static/0000755000175100001770000000000014706731615015643 5ustar runnerdocker00000000000000mido-1.3.3/docs/_static/.gitkeep0000644000175100001770000000007014706731605017270 0ustar runnerdocker00000000000000This file is needed for git to include this directory. mido-1.3.3/docs/about_midi.rst0000644000175100001770000000523714706731605017071 0ustar runnerdocker00000000000000.. SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen .. .. SPDX-License-Identifier: CC-BY-4.0 About MIDI ========== A Short Introduction To MIDI ---------------------------- MIDI is a simple binary protocol for communicating with synthesizers and other electronic music equipment. It was developed in 1981 by Dave Smith and Chet Wood of Sequential Systems. MIDI was quickly embraced by all the major synth manufacturers and led to developments such as microcomputer sequencers, and with them the electronic home studio. Although many attempts have been made to replace it, it is still the industry standard. MIDI was designed for the 8-bit micro controllers found in synthesizers at the beginning of the 80's. As such, it is a very minimal byte-oriented protocol. The message for turning a note on is only three bytes long (here shown in hexadecimal): .. code-block:: text 92 3C 64 This message consists of: .. code-blocK:: text 92 -- 9 == message type note on 2 == channel 2 3C -- note 60 (middle C) 64 -- velocity (how hard the note is hit) The first byte is called a ``status`` byte. It has the upper bit set, which is how you can tell it apart from the following ``data`` bytes. Data bytes are thus *always* 7 bits (Values: 0..127). Each message type has a given number of data bytes, the exception being the :term:`System Exclusive` message which has a start (``SOX``) and a stop (``EOX``) byte and any number of data bytes in-between these two: .. code-block:: text F0 ... F7 Messages can be divided into four groups: * Channel Messages. These are used to turn notes on and off, to change patches, and change controllers (pitch bend, modulation wheel, pedal and many others). There are 16 channels, and the channel number is encoded in the lower 4 bits (aka :term:`nibble`) of the status byte. Each synth can choose which channel (or channels) it responds to. This can typically be configured. * System Common Messages. * System Real Time Messages, includes ``start``, ``stop``, ``continue``, ``song position`` (for playback of songs) and ``reset``. * System Exclusive Messages (often called :term:`SysEx` messages) are used for sending and receiving *device specific* data such as patches and proprietary controls. Some Examples of Messages ------------------------- .. code-block:: text # Turn on middle C on channel 2: 92 3C 64 # Turn it back off: 82 3C 64 # Change to program (sound) number 4 on channel 2: C2 04 # Continue (Starts a song that has been paused): FB # Sysex data request for the Roland SH-201 synthesizer: F0 41 10 00 00 16 11 20 00 00 00 00 00 00 21 3F F7 mido-1.3.3/docs/acknowledgements.rst0000644000175100001770000000056014706731605020301 0ustar runnerdocker00000000000000.. SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen .. .. SPDX-License-Identifier: CC-BY-4.0 Acknowledgments =============== Thanks to ``/u/tialpoy/`` on Reddit for extensive code review and helpful suggestions. Thanks to everyone who has sent bug reports and patches. The ``PortMidi`` wrapper is based on ``portmidizero`` by Grant Yoshida. mido-1.3.3/docs/api.rst0000644000175100001770000000606114706731605015522 0ustar runnerdocker00000000000000.. SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen .. SPDX-FileCopyrightText: 2023 Raphaël Doursenaud .. .. SPDX-License-Identifier: CC-BY-4.0 .. _api: API Reference ============= Messages -------- .. module:: mido .. autoclass:: Message :members: :inherited-members: :undoc-members: .. module:: mido.messages .. todo:: Expose more of the internals? (Checks, decode…) Frozen Messages ^^^^^^^^^^^^^^^ .. module:: mido.frozen .. autofunction:: freeze_message .. autofunction:: thaw_message .. autofunction:: is_frozen .. autoclass:: Frozen .. autoclass:: FrozenMessage .. autoclass:: FrozenMetaMessage .. autoclass:: FrozenUnknownMetaMessage Parsing ------- .. module:: mido.parser .. autofunction:: parse .. autofunction:: parse_all .. autoclass:: Parser :members: :inherited-members: :undoc-members: Tokenizing ---------- .. module:: mido.tokenizer .. autoclass:: Tokenizer :members: :inherited-members: :undoc-members: .. todo: Add a dedicated section in the documentation. Backends -------- .. module:: mido :noindex: .. autofunction:: set_backend .. autoclass:: Backend :members: :inherited-members: :undoc-members: .. module:: mido.backends .. todo:: Expose each built-in backend internal API? Ports ----- Management ^^^^^^^^^^ .. module:: mido :noindex: .. autofunction:: open_input .. autofunction:: open_output .. autofunction:: open_ioport .. autofunction:: get_input_names .. autofunction:: get_output_names .. autofunction:: get_ioport_names Socket Ports ^^^^^^^^^^^^ .. module:: mido.sockets .. autoclass:: PortServer :members: :inherited-members: :undoc-members: .. autoclass:: SocketPort :members: :inherited-members: :undoc-members: .. autofunction:: parse_address API ^^^ .. module:: mido.ports .. autoclass:: BaseInput :members: :inherited-members: :undoc-members: .. autoclass:: BaseOutput :members: :inherited-members: :undoc-members: .. autoclass:: IOPort :members: :inherited-members: :undoc-members: .. autoclass:: MultiPort :members: :inherited-members: :undoc-members: .. autofunction:: multi_receive .. autofunction:: multi_iter_pending .. autofunction:: multi_send .. autofunction:: sleep .. autofunction:: set_sleep_time .. autofunction:: get_sleep_time .. autofunction:: panic_messages .. autofunction:: reset_messages Files ------ Standard MIDI Files ^^^^^^^^^^^^^^^^^^^ .. module:: mido :noindex: .. autoclass:: MidiFile :members: :inherited-members: :undoc-members: .. autoclass:: MidiTrack :members: :inherited-members: :undoc-members: .. autoclass:: MetaMessage :members: :inherited-members: :undoc-members: .. autofunction:: tick2second .. autofunction:: second2tick .. autofunction:: bpm2tempo .. autofunction:: tempo2bpm .. autofunction:: merge_tracks .. module:: mido.midifiles .. todo: Expose more of the internal API? (meta, tracks, units…) SYX ^^^ .. module:: mido.syx .. autofunction:: read_syx_file .. autofunction:: write_syx_file mido-1.3.3/docs/authors.rst0000644000175100001770000000105014706731605016427 0ustar runnerdocker00000000000000.. SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen .. SPDX-FileCopyrightText: 2023 Raphaël Doursenaud .. .. SPDX-License-Identifier: CC-BY-4.0 Authors ======= Ole Martin Bjørndalen (lead programmer), Raphaël Doursenaud (co-maintainer) and many other contributors. Many people have contributed to Mido over the years, but this page has not been updated to include them. The :doc:`/changes` page includes names of all contributors. .. seealso:: https://github.com/mido/mido/graphs/contributors mido-1.3.3/docs/backends/0000755000175100001770000000000014706731615015767 5ustar runnerdocker00000000000000mido-1.3.3/docs/backends/amidi.rst0000644000175100001770000000251114706731605017602 0ustar runnerdocker00000000000000.. SPDX-FileCopyrightText: 2017 Ole Martin Bjorndalen .. .. SPDX-License-Identifier: CC-BY-4.0 amidi (Experimental) -------------------- Name: ``mido.backends.amidi`` Resources: * `The Advanced Linux Sound Architecture (ALSA) project `_ * `ALSA Opensrc Org amidi `_ Features ^^^^^^^^ * Linux only. * very basic implementation. * no callbacks * can only access physical ports. (Devices that are plugged-in.) * high overhead when sending since it runs a new ``amidi`` command for each message. * known bug: is one behind when receiving messages. See below. Operation ^^^^^^^^^ The ``amidi`` command (part of ALSA and the `alsa-utils` package) is used for I/O: * ``amidi -l`` to list messages (in ``get_input_names()`` etc.) * ``amidi -d -p DEVICE`` to receive messages. ``amidi`` prints these out one on each line as hex bytes. Unfortunately it puts the newline at the beginning of the line which flushes the buffer before the message instead of after. This causes problems with non-blocking receiption using ``select.poll()`` which means messages are received one behind. This needs to be looked into. * ``amidi --send-hex MESSAGE_IN_HEX -p DEVICE`` to send messages. Since this is called for every message the overhead is very high. mido-1.3.3/docs/backends/custom.rst0000644000175100001770000000251514706731605020035 0ustar runnerdocker00000000000000.. SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen .. .. SPDX-License-Identifier: CC-BY-4.0 Writing a New or Custom Backend ------------------------------- A backend is a Python module with one or more of these:: Input -- an input port class Output -- an output port class IOPort -- an I/O port class get_devices() -- returns a list of devices Once written, the backend can be used by setting the environment variable ``MIDO_BACKEND`` or by calling ``mido.set_backend()``. In both cases, the path of the module is used. ``Input`` And input class for ``open_input()``. This is only required if the backend supports input. ``Output`` And output class for ``open_output()``. This is only required if the backend supports output. ``IOPort`` An I/O port class for ``open_ioport()``. If this is not found, ``open_ioport()`` will return ``mido.ports.IOPort(Input(), Output())``. ``get_devices(**kwargs)`` Returns a list of devices, where each device is dictionary with at least these three values:: { 'name': 'Some MIDI Input Port', 'is_input': True, 'is_output': False, } These are used to build return values for ``get_input_names()`` etc.. This function will also be available to the user directly. For examples, see ``mido/backends/``. mido-1.3.3/docs/backends/index.rst0000644000175100001770000000574114706731605017636 0ustar runnerdocker00000000000000.. SPDX-FileCopyrightText: 2017 Ole Martin Bjorndalen .. .. SPDX-License-Identifier: CC-BY-4.0 Backends ======== A backend provides the interface between Mido and the operating system level MIDI stack. Some Mido features are only available with select backends. Mido's backend subsystem has been designed to be extensible so you can add your own backends if required. See :doc:`custom`. Providing platform specific Python-native backends is currently evaluated. See: https://github.com/mido/mido/issues/506 .. todo:: Insert a stack diagram to clear things up. Choice ------ Mido comes with five backends: * :doc:`RtMidi ` is the *default* and *recommended* backend. It has all the features of the other ones and more plus it is usually easier to install. * :doc:`PortMidi ` was the default backend up until version 1.2. It uses the ``portmidi`` shared library and can be difficult to install on some systems. * :doc:`Pygame ` uses the ``pygame.midi`` module. * :doc:`rtmidi-python ` uses the ``rtmidi_python`` package, an alternative wrapper for PortMidi. It is currently very basic but easier to install on some Windows systems. * :doc:`Amidi ` is an experimental backend for Linux/ALSA that uses the command ``amidi`` to send and receive messages. You can set the backend using an environment variable: See :ref:`env_vars`. Alternatively, you can set the backend from within your program:: >>> mido.set_backend('mido.backends.portmidi') >>> mido.backend .. note:: This will override the environment variable. If you want to use more than one backend at a time, you can do:: rtmidi = mido.Backend('mido.backends.rtmidi') portmidi = mido.Backend('mido.backends.portmidi') input = rtmidi.open_input() output = portmidi.open_output() for message in input: output.send(message) The backend will not be loaded until you call one of the ``open_`` or ``get_`` methods. You can pass ``load=True`` to have it loaded right away. If you pass ``use_environ=True``, the module will use the environment variables ``MIDO_DEFAULT_INPUT`` etc. for default ports. .. _env_vars: Environment Variables --------------------- Select Backend ^^^^^^^^^^^^^^ If you want to use a backend other than RtMidi you can override this with the ``MIDO_BACKEND`` environment variable, for example:: $ MIDO_BACKEND=mido.backends.portmidi ./program.py Set Default ports ^^^^^^^^^^^^^^^^^ You can override the backend's choice of default ports with these three environment variables:: MIDO_DEFAULT_INPUT MIDO_DEFAULT_OUTPUT MIDO_DEFAULT_IOPORT For example:: $ MIDO_DEFAULT_INPUT='SH-201' python3 program.py or:: $ export MIDO_DEFAULT_OUTPUT='Integra-7' $ python3 program1.py $ python3 program2.py Available Backends ------------------ .. toctree:: rtmidi portmidi pygame rtmidi_python amidi .. include:: custom.rst mido-1.3.3/docs/backends/portmidi.rst0000644000175100001770000000212614706731605020350 0ustar runnerdocker00000000000000.. SPDX-FileCopyrightText: 2017 Ole Martin Bjorndalen .. .. SPDX-License-Identifier: CC-BY-4.0 PortMidi -------- Name: ``mido.backends.portmidi`` Resources: * `PortMidi C Library `_ Installing ^^^^^^^^^^ The PortMidi backend requires the ``portmidi`` shared library. `Ubuntu `_:: apt install libportmidi-dev `Homebrew `_:: brew install portmidi `MacPorts `_:: port install portmidi The backend will look for:: portmidi.so (Linux) portmidi.dynlib (macOS) portmidi.dll (Windows) Features ^^^^^^^^ * Can send but doesn't receive ``active_sensing`` messages. * No callback mechanism so callbacks are implemented in Python with threads. Each port with a callback has a dedicated thread doing blocking reads from the device. * Due to limitations in PortMidi the port list will not be up-to-date if there are any ports open. (The refresh is implemented by re-initializing PortMidi which would break any open ports.) mido-1.3.3/docs/backends/pygame.rst0000644000175100001770000000104314706731605020000 0ustar runnerdocker00000000000000.. SPDX-FileCopyrightText: 2017 Ole Martin Bjorndalen .. .. SPDX-License-Identifier: CC-BY-4.0 Pygame ------ Name: ``mido.backends.pygame`` Resources: * `PyGame Python Library `_ * `PortMidi C Library `_ The Pygame backend uses the `pygame.midi `_ module for I/O. Features ^^^^^^^^ * Doesn't receive ``active_sensing``. * Callbacks are currently not implemented. * Pygame.midi is implemented on top of PortMidi. mido-1.3.3/docs/backends/rtmidi.rst0000644000175100001770000001025214706731605020010 0ustar runnerdocker00000000000000.. SPDX-FileCopyrightText: 2017 Ole Martin Bjorndalen .. .. SPDX-License-Identifier: CC-BY-4.0 RtMidi (Default, Recommended) ----------------------------- Name: ``mido.backends.rtmidi`` Resources: * `python-rtmidi Python Library `_ * `RtMidi C Library `_ The RtMidi backend is a thin wrapper around `python-rtmidi `_. Features ^^^^^^^^ * callbacks * true blocking ``receive()`` in Python 3 (using a *callback* and a *queue*) * virtual ports (Except on Microsoft Windows) * ports can be opened multiple times, each will receive a copy of all messages * a *client name* can be specified when opening a virtual port * sends but doesn't receive active sensing (By default) * port list is always up to date * all methods but ``close()`` are thread safe Port Names (Linux/ALSA) ^^^^^^^^^^^^^^^^^^^^^^^ When you're using Linux/ALSA the port names include client name and ALSA client and port numbers, for example: .. code-block:: python >>> mido.get_output_names() ['TiMidity:TiMidity port 0 128:0'] The ALSA client and port numbers ("``128:0``" in this case) can change from session to session, making it hard to hard code port names or use them in configuration files. To get around this the RtMidi backend allows you to leave out the port number of port number and client names. These lines will all open the same port as above: .. code-block:: python mido.open_output('TiMidity port 0') .. code-block:: python mido.open_output('TiMidity:TiMidity port 0') .. code-block:: python mido.open_output('TiMidity:TiMidity port 0 128:0') There is currently no way to list ports without port number or client name. This can be added in a future version of there is demand for it and a suitable API is found. Virtual Ports ^^^^^^^^^^^^^ RtMidi is the only backend that can create virtual ports: .. code-block:: python >>> port = mido.open_input('New Port', virtual=True) >>> port Other applications can now connect to this port. (One oddity is that, at least in Linux, RtMidi can't see its own virtual ports, while PortMidi can see them.) .. note:: Virtual Ports are **not** available under Microsoft Windows. An alternative is to use third party software such as Tobias Erichsen's `loopMIDI `_. Client Name ^^^^^^^^^^^ .. versionadded:: 1.2 You can specify a client name for the port: .. code-block:: python >>> port = mido.open_input('New Port', client_name='My Client') This requires ``python-rtmidi >= 1.0rc1``. If ``client_name`` is passed the port will be a virtual port. .. note:: Unfortunately, at least with ALSA, opening two ports with the same ``client_name`` creates two clients with the same name instead of one client with two ports. There are a couple of problems with port names in Linux. First, RtMidi can't see some software ports such as ``amSynth MIDI IN``. PortMidi uses the same ALSA sequencer API, so this is problem in RtMidi. Second, in some versions of RtMidi ports are named inconsistently. For example, the input port '``Midi Through 14:0``' has a corresponding output named '``Midi Through:0``'. Unless this was intended, it is a bug in RtMidi's ALSA implementation. Choosing an API ^^^^^^^^^^^^^^^ The RtMidi library can be compiled with support for more than one API. To get a list of all available APIs at runtime:: >>> mido.backend.module.get_api_names() ['LINUX_ALSA', 'UNIX_JACK'] You can select the API by adding it after the module name, either in the environment variable:: $ export MIDO_BACKEND=mido.backends.rtmidi/LINUX_ALSA $ export MIDO_BACKEND=mido.backends.rtmidi/UNIX_JACK or within the program using one of these:: >>> mido.set_backend('mido.backends.rtmidi/LINUX_ALSA') >>> mido.backend >>> mido.Backend('mido.backends.rtmidi/UNIX_JACK') This allows you to, for example, use both ALSA and JACK ports in the same program. mido-1.3.3/docs/backends/rtmidi_python.rst0000644000175100001770000000134614706731605021415 0ustar runnerdocker00000000000000.. SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen .. .. SPDX-License-Identifier: CC-BY-4.0 rtmidi_python ------------- Name: ``mido.backends.rtmidi_python`` Resources: * `rtmidi-python Python Library `_ Installing ^^^^^^^^^^ :: python3 - m pip install rtmidi-python Features ^^^^^^^^ * uses the ``rtmidi_python`` package rather than ``python-rtmidi`` * supports callbacks * limited support for virtual ports (no client name) * no true blocking * sends but doesn't receive ``active sensing`` .. todo:: Since the API of ``rtmidi_python`` and ``python-rtmidi`` are almost identical it would make sense to refactor so they share most of the code. mido-1.3.3/docs/binaries.rst0000644000175100001770000000331014706731605016537 0ustar runnerdocker00000000000000.. SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen .. .. SPDX-License-Identifier: CC-BY-4.0 Included Programs ================= A few sample programs are installed with Mido and available directly from the :term:`CLI`. .. warning:: These are intended to demonstrate the capabilities of Mido and used as a template for your own programs. These are not fully fledged and may miss crucial features. mido-ports ---------- Lists all available input and output ports, shows environment variables and the current backend module. mido-play --------- Plays back one or more MIDI files:: $ mido-play song1.mid [song2.mid] mido-serve ---------- Serves one or more ports over the network, for example:: $ mido-serve :9080 'Integra-7' You can now connect to this port with ``mido-forward`` (or use ``mido.sockets.connect()`` and send messages to it. The messages will be forwarded to every port you listed (in this case 'Integra-7'). mido-connect ------------ Forwards all messages that arrive on one or more ports to a server. For example, to use the SH-201 keyboard connected to this computer to play sounds on the Integra-7 on a computer named ``mac.local`` (which runs the server as above), you can do:: $ mido-connect mac.local:9080 'SH-201' Note that you may experience latency and jitter, so this may not be very useful for live playing or for playing back songs. There is also no security built in, so you should only use this on a trusted network. (Anyone can connect and send anything, including harmful sysex messages.) ``mido-serve`` and ``mido-connect`` are only included as fun programs to play with, but may in the future be expanded into something more usable. mido-1.3.3/docs/changes.rst0000644000175100001770000006530714706731605016371 0ustar runnerdocker00000000000000.. SPDX-FileCopyrightText: 2014 Ole Martin Bjorndalen .. .. SPDX-License-Identifier: CC-BY-4.0 Version Changes =============== This project uses `Semantic Versioning `_. .. seealso:: The `Future `_ milestone on Github for future plans. Release History --------------- 1.3.3 (2024-10-25) ^^^^^^^^^^^^^^^^^^ * New Python 3.13 support * Bugfix Unpinned hard dependencies to avoid conflicts * Documentation Fixed broken external links 1.3.2 (2023-12-15) ^^^^^^^^^^^^^^^^^^ * Bugfix Midifile: skip_checks were missing on MetaMessage (@almostimplemented) * Bugfix Scripts/mido-play: UnboundLocalError (@rdoursenaud) * Minor documentation cleanup (@rdoursenaud) 1.3.1 (2023-12-14) ^^^^^^^^^^^^^^^^^^ * New Python 3.12 support * Improved merged_tracks performance (@almostimplemented, pull request #474) * Bugfix Backends/portmidi: SysEx messages were generating an error (@tweakoz, pull request #523) * Bugfix Midifile: Defer computing merged_track (@akx, pull request #565) * Bugfix pip release: Scripts can now be executed properly (@rdoursenaud) * Tooling: Linting now uses ruff (@akx, pull requests #564, #566) * Minor documentation improvements (@rdoursenaud) 1.3.0 (2023-07-21) ^^^^^^^^^^^^^^^^^^ .. warning:: This release drops support for Python 2 and is only compatible with 3.7 onwards. * Bugfix Backends/rtmidi: Prevent virtual port name mangling (@rdoursenaud, thanks to @digitalsignalperson for reporting) * Bugfix Backends/rtmidi: Remove callback before closing the port to avoid a race condition (@rdoursenaud) * Bugfix MidiFile: Properly decode/encode SMPTE hours in the SMPTE offset Meta (Thanks to @laori93 for reporting and @heilei for investigating. Issue #156) * Installation: support the "extras" syntax to install optional dependencies (@rdoursenaud) * Documentation: updated, overhauled and proofread (@rdoursenaud, nomadbyte, @superbock) * Bugfix: Backends/Portmidi (@akx, pull request #483) * MidiFile: Move merging track out of ``__iter__()`` to prevent hanging on first call (@Frnot, pull request #470) * MidiFile: ``play()`` can now use an optional custom clock source (@almostimplemented, pull request #153) * The project is now REUSE compliant. See https://reuse.software/ for details (@rdoursenaud) * Packaging is now PEP-518 compliant (@rdoursenaud) * Backends/Socket: Disable buffering (@m-vo, pull request #342) * Removed support for Python 2.7. * Mido now requires Python 3.7 or higher. (Ole Martin Bjørndalen, pull request #408, with additional cleanup from @rdoursenaud) * Backends: The ``rtmidi`` and ``python-rtmidi`` 1.2.10 sometimes returned duplicate port names. (Bug introduced in 1.2.10. Fix by Maciej Sokołowski, pull request #321) * Bugfix Backends/Socket: In Python 3, PortServer used to crashe when a socket client disconnects. (issue #290) (@kyleclaassen, pull request #291) * MidiFile: Make ``UnknownMetaMessage`` robust to faulty MIDI files (@sonovice, pull request #286) * Bugfix MIDIFile: BPM <-> MIDI tempo conversions (@superbock, pull request #114) * MidiFile: Added ``from_bytes()`` to ``MetaMessage`` (@gulaki, pull request #149) 1.2.10 (2021-05-10) ^^^^^^^^^^^^^^^^^^^ * New ``repr()`` format for messages, tracks and MIDI file objects. (Implemented by John Belmonte, pull request #164.) * added new example ``midifiles/show_midifile.py`` based on the new ``repr()`` format. * Added ``msg.is_cc()`` method. Checks if message is a control change. Can also be used to check for a specific control change number, for example ``msg.is_cc(7)``. * Fixed memory leaks in RtMidi backend (issue #256, fix by The Other Days, pull request #264.) * clip now works with sysex messages (Fix by Avatar Timo Stüber, pull request #229.) * Improved docs and error message for time attribute in a message. (tomerv, pull request #249.) * Improved MidiFile.play to avoid time drift. (Implemented by John Belmonte, pull request #161.) * bugfix: MIDO_DEFAULT_INPUT was misspelled in mido-ports causing it to be show as 'not set' even though it was set. (Fix by Bernhard Wagner, pull request #192.) * Now only copies ports once in ports.multi_receive() (Tom Ritchford, pull request #191.) * Ports lists returned from ``get_input_names()`` and friends are no longer sorted. (Suggested and implemented by Ryan McCampbell, issue #298.) * Updated linke in docs to point to the new home github.com/mido/ (Fixed by Joshua Mayers, pull request #177.) * thanks to Christopher Arndt, Kathryn DiPippo and Timo Stüber for fixing flake8 issues. 1.2.9 (2018-10-05) ^^^^^^^^^^^^^^^^^^ * rewrote ``Parser`` class around a MIDI tokenizer. Should lead to slight speedup and much cleaner code. * bugfix: `data` attribute was missing for `UnknownMetaMessage` objects. This caused `AttributeError` when the messages were printed or saved to a file. Also, the documentation incorrectly listed the attribute as `_data` instead of `data`. (Reported by Groowy.) * bugfix: UnknownMetaMessage encoding was broken causing crashes when saving a file with unknown meta messages. (Reported by exeex, issue #159.) * bugfix: inputs and outputs were switched around when opening named ports with PortMidi backend. (Reproduced by Predrag Radovic, issue #108, fix by Juan Antonio Aldea, pull request #109.) * bugfix: time signature meta messages had wrong default value of 2/4. The default value is now 4/4. (Fix by Sebastian Böck, pull request #104.) * bugfix: ``msg.copy()`` didn't handle generators for sysex data. ``msg.copy(data=(i for i in range(3)))`` would give ``data=()`` instead of ``data=(0,1,2)``. (The code should be refactored so this is handled by the same function everywhere, such as in ``__init__()``, in ``copy()`` and in ``parser.feed()``.) * bugfix: ``MultiPort._receive()`` ignored the ``block`` parameter. (Fix by Tom Swirly, pull request #135.) * bugfix: sequencer number meta message was incorrectly limited to range 0..255 instead of 0..65335. (Reported by muranyia, issue #144.) * now using Tox for testing. (Implemented by Chris Apple, pull request #123.) * Travis integration up by Carl Thomé and Chris Apple. 1.2.8 (2017-06-30) ^^^^^^^^^^^^^^^^^^ * bugfix: nonblocking receive was broken for RtMidi IO ports. (Reported by Chris Apple, issue #99.) * bugfix: ``IOPort.poll()`` would block if another thread was waiting for ``receive()``. Fixed the problem by removing the lock, which was never needed in the first place as the embedded input port does its own locking. 1.2.7 (2017-05-31) ^^^^^^^^^^^^^^^^^^ * added max length when reading message from a MIDI file. This prevents Python from running out of memory when reading a corrupt file. Instead it will now raise an ``IOError`` with a descriptive error message. (Implemented by Curtis Hawthorne, pull request #95.) * removed dependency on ``python-rtmidi`` from tests. (Reported by Josue Ortega, issue #96.) 1.2.6 (2017-05-04) ^^^^^^^^^^^^^^^^^^ * bugfix: Sending sysex with Pygame in Python 3 failed with ``"TypeError: array() argument 1 must be a unicode character, not byte"``. (Reported by Harry Williamson.) * now handles ``sequence_number`` and ``midi_port`` messages with 0 data bytes. These are incorrect but can occur in rare cases. See ``mido/midifiles/test_midifiles.py`` for more. (Reported by Gilthans (issue #42) and hyst329 (issue #93)). 1.2.5 (2017-04-28) ^^^^^^^^^^^^^^^^^^ * bugfix: RtMidi backend ignored ``api`` argument. (Fix by Tom Feist, pull request #91.) 1.2.4 (2017-03-19) ^^^^^^^^^^^^^^^^^^ * fixed outdated python-rtmidi install instructions. (Reported by Christopher Arndt, issue #87.) 1.2.3 (2017-03-14) ^^^^^^^^^^^^^^^^^^ * typo and incorrect links in docs fixed by Michael (miketwo) (pull requests #84 and #85). 1.2.2 (2017-03-14) ^^^^^^^^^^^^^^^^^^ * bugfix: sysex data was broken in string format encoding and decoding. The data was encoded with spaces ('data=(1, 2, 3)') instead of as one word ('data=(1,2,3)'). * added some tests for string format. * bugfix: ``BaseOutput.send()`` raised string instead of ``ValueError``. 1.2.1 (2017-03-10) ^^^^^^^^^^^^^^^^^^ * bugfix: IO port never received anything when used with RtMidi backend. (Reported by dagargo, issue #83.) This was caused by a very old bug introduced in 1.0.3. IOPort mistakenly called the inner method ``self.input._receive()`` instead of ``self.input.receive()``. This happens to work for ports that override ``_receive()`` but not for the new RtMidi backend which overrides ``receive()``. (The default implementation of ``_receive()`` just drops the message on the floor.) * bugfix: PortMidi backend was broken due to missing import (``ctypes.byref``). (Introduced in 1.2.0.) 1.2.0 (2017-03-07) ^^^^^^^^^^^^^^^^^^^ New implementation of messages and parser: * completely reimplemented messages. The code is now much simpler, clearer and easier to work with. * new contructors ``Message.from_bytes()``, ``Message.from_hex()``, ``Message.from_str()``. * new message attributes ``is_meta`` and ``is_realtime``. Frozen (immutable) messages: * added ``FrozenMessage`` and ``FrozenMetaMessage``. These are immutable versions of ``Message`` and ``MetaMessage`` that are hashable and thus can be used as dictionary keys. These are available in ``mido.frozen``. (Requested by Jasper Lyons, issue #36.) RtMidi is now the default backend: * switched default backend from PortMidi to RtMidi. RtMidi is easier to install on most systems and better in every way. If you want to stick to PortMidi you can either set the environment variable ``$MIDO_BACKEND=mido.backends.portmidi`` or call ``mido.set_backend('mido.backends.portmidi')`` in your program. * refactored the RtMidi backend to have a single ``Port`` class instead of inheriting from base ports. It was getting hard to keep track of it all. The code is now a lot easier to reason about. * you can now pass ``client_name`` when opening RtMidi ports: ``open_output('Test', client_name='My Client')``. When ``client_name`` is passed the port will automatically be a virtual port. * with ``LINUX_ALSA`` you can now omit client name and ALSA client/port number when opening ports, allowing you to do ``mido.open_output('TiMidity port 0')`` instead of ``mido.open_output('TiMidity:TiMidity port 0 128:0')``. (See RtMidi backend docs for more.) Changes to the port API: * ports now have ``is_input`` and ``is_output`` attributes. * new functions ``tick2second()`` and ``second2tick()``. (By Carl Thomé, pull request #71.) * added ``_locking`` attribute to ``BasePort``. You can set this to ``False`` in a subclass to do your own locking. * ``_receive()`` is now allowed to return a messages. This makes the API more consistent and makes it easier to implement thread safe ports. * ``pending()`` is gone. This had to be done to allow for the new ``_receive()`` behavior. * improved MIDI file documentation. (Written by Carl Thomé.) Other changes: * bugfix: if a port inherited from both ``BaseInput`` and ``BaseOutput`` this would cause ``BasePort.__init__()`` to be called twice, which means ``self._open()`` was also called twice. As a workaround ``BasePort.__init__()`` will check if ``self.closed`` exists. * added ``mido.version_info``. * ``mido.set_backend()`` can now be called with ``load=True``. * added ``multi_send()``. * ``MIN_PITCHWHEEL``, ``MAX_PITCHWHEEL``, ``MIN_SONGPOS`` and ``MAX_SONGPOS`` are now available in the top level module (for example ``mido.MIN_PITCHWHEEL``). * added experimental new backend ``mido.backends.amidi``. This uses the ALSA ``amidi`` command to send and receive messages, which makes it very inefficient but possibly useful for sysex transfer. * added new backend ``mido.backends.rtmidi_python`` (previously available in the examples folder.) This uses the ``rtmidi-python`` package instead of ``python-rtmidi``. For now it lacks some of features of the ``rtmidi`` backend, but can still be useful on systems where ``python-rtmidi`` is not available. (Requested by netchose, issue #55.) 1.1.24 (2017-02-16) ^^^^^^^^^^^^^^^^^^^ * bugfix: PortMidi backend was broken on macOS due to a typo. (Fix by Sylvain Le Groux, pull request #81.) 1.1.23 (2017-01-31) ^^^^^^^^^^^^^^^^^^^ * bugfix: ``read_syx_file()`` didn't handle '\n' in text format file causing it to crash. (Reported by Paul Forgey, issue #80.) 1.1.22 (2017-01-27) ^^^^^^^^^^^^^^^^^^^ * the bugfix in 1.1.20 broke blocking receive() for RtMidi. Reverting the changes. This will need some more investigation. 1.1.21 (2017-01-26) ^^^^^^^^^^^^^^^^^^^ * bugfix: MidiFile save was broken in 1.1.20 due to a missing import. 1.1.20 (2017-01-26) ^^^^^^^^^^^^^^^^^^^ * bugfix: close() would sometimes hang for RtMidi input ports. (The bug was introduced in 1.1.18 when the backend was rewritten to support true blocking.) * Numpy numbers can now be used for all message attributes. (Based on implementation by Henry Mao, pull request #78.) The code checks against numbers.Integral and numbers.Real (for the time attribute) so values can be any subclass of these. 1.1.19 (2017-01-25) ^^^^^^^^^^^^^^^^^^^ * Pygame backend can now receive sysex messages. (Fix by Box of Stops.) * bugfix: ``libportmidi.dylib`` was not found when using MacPorts. (Fix by yam655, issue #77.) * bugfix: ``SocketPort.__init()`` was not calling ``IOPort.__init__()`` which means it didn't get a ``self._lock``. (Fixed by K Lars Lohn, pull request #72. Also reported by John J. Foerch, issue #79.) * fixed typo in intro example (README and index.rst). Fix by Antonio Ospite (pull request #70), James McDermott (pull request #73) and Zdravko Bozakov (pull request #74). * fixed typo in virtual ports example (Zdravko Bozakov, pull request #75.) 1.1.18 (2016-10-22) ^^^^^^^^^^^^^^^^^^^ * ``time`` is included in message comparison. ``msg1 == msg2`` will now give the same result as ``str(msg1) == str(msg2)`` and ``repr(msg1)`` == ``repr(msg2)``. This means you can now compare tracks wihout any trickery, for example: ``mid1.tracks == mid2.tracks``. If you need to leave out time the easiest was is ``msg1.bytes() == msg2.bytes()``. This may in rare cases break code. * bugfix: ``end_of_track`` messages in MIDI files were not handled correctly. (Reported by Colin Raffel, issue #62). * bugfix: ``merge_tracks()`` dropped messages after the first ``end_of_track`` message. The new implementation removes all ``end_of_track`` messages and adds one at the end, making sure to adjust the delta times of the remaining messages. * refactored MIDI file code. * ``mido-play`` now has a new option ``-m / --print-messages`` which prints messages as they are played back. * renamed ``parser._parsed_messages`` to ``parser.messages``. ``BaseInput`` and ``SocketPort`` use it so it should be public. * ``Parser()`` now takes an option argument ``data`` which is passed to ``feed()``. 1.1.17 (2016-10-06) ^^^^^^^^^^^^^^^^^^^ * RtMidi now supports true blocking ``receive()`` in Python 3. This should result in better performance and lower latency. (Thanks to Adam Roberts for helping research queue behavior. See issue #49 for more.) * bugfix: ``MidiTrack.copy()`` (Python 3 only) returned ``list``. * fixed example ``queue_port.py`` which broke when locks where added. 1.1.16 (2016-09-27) ^^^^^^^^^^^^^^^^^^^ * bugfix: ``MidiTrack`` crashed instead of returning a message on ``track[index]``. Fix by Colin Raffel (pull request #61). * added ``__add__()`` and ``__mul__()`` to ``MidiTrack`` so ``+`` and ``*`` will return tracks instead of lists. * added ``poll()`` method to input ports as a shortcut for ``receive(block=False)``. * added example ``rtmidi_python_backend.py``, a backend for the rtmidi-python package (which is different from the python-rtmidi backend that Mido currently uses.) This may at some point be added to the package but for now it's in the examples folder. (Requested by netchose, issue #55.) * removed custom ``_import_module()``. Its only function was to make import errors more informative by showing the full module path, such as ``ImportError: mido.backends.rtmidi`` instead of just ``ImportError: rtmidi``. Unfortunately it ended up masking import errors in the backend module, causing confusion. It turns ``importlib.import_module()`` can be called with the full path, and on Python 3 it will also display the full path in the ``ImportError`` message. 1.1.15 (2016-08-24) ^^^^^^^^^^^^^^^^^^^ * Sending and receiving messages is now thread safe. (Initial implementation by Adam Roberts.) * Bugfix: ``PortServer`` called ``__init__`` from the wrong class. (Fix by Nathan Hurst.) * Changes to ``MidiTrack``: * ``MidiTrack()`` now takes a as a parameter an iterable of messages. Examples: .. code-block:: python MidiTrack(messages) MidiTrack(port.iter_pending()) MidiTrack(msg for msg in some_generator) * Slicing a ``MidiTrack`` returns a ``MidiTrack``. (It used to return a ``list``.) Example: .. code-block:: python track[1:10] * Added the ability to use file objects as well as filenames when reading, writing and saving MIDI files. This allows you to create a MIDI file dynamically, possibly *not* using mido, save it to an io.BytesIO, and then play that in-memory file, without having to create an intermediate external file. Of course the memory file (and/or the MidiFile) can still be saved to an external file. (Implemented by Brian O'Neill.) * PortMidi backend now uses pm.lib.Pm_GetHostErrorText() to get host error messages instead of just the generic "PortMidi: \`Host error\'". (Implemented by Tom Manderson.) Thanks to Richard Vogl and Tim Cook for reporting errors in the docs. 1.1.14 (2015-06-09) ^^^^^^^^^^^^^^^^^^^ * bugfix: merge_tracks() concatenated the tracks instead of merging them. This caused tracks to be played back one by one. (Issue #28, reported by Charles Gillingham.) * added support for running status when writing MIDI files. (Implemented by John Benediktsson.) * rewrote the callback system in response to issues #23 and #25. * there was no way to set a callback function if the port was opened without one. (Issue#25, reported by Nils Werner.) Callbacks can now be set and cleared at any time by either passing one to ``open_input()`` or updating the ``callback`` attribute. This causes some slight changes to the behavior of the port when using callbacks. Previously if you opened the port with a callback and then set ``port.callback = None`` the callback thread would keep running but drop any incoming messages. If you do the same now the callback thread will stop and the port will return normal non-callback behavior. If you want the callback thread to drop messages you can set ``port.callback = lambda message: None``. Also, ``receive()`` no longer checks ``self.callback``. This was inconsistent as it was the only method to do so. It also allows ports that don't support callbacks to omit the ``callback`` attribute. * bugfix: closing a port would sometimes cause a segfault when using callbacks. (Issue #24, reported by Francesco Ceruti.) * bugfix: Pygame ports were broken due to a faulty check for ``virtual=True``. * now raises ``ValueError`` instead of ``IOError`` if you pass ``virtual`` or ``callback`` while opening a port and the backend doesn't support them. (An unsupported argument is not an IO error.) * fixed some errors in backend documentation. (Pull request #23 by velolala.) * ``MultiPort`` now has a ``yield_port`` argument just like ``multi_receive()``. 1.1.13 (2015-02-07) ^^^^^^^^^^^^^^^^^^^ * the PortMidi backend will now return refresh the port list when you ask for port names are open a new port, which means you will see devices that you plug in after loading the backend. (Due to limitations in PortMidi the list will only be refreshed if there are no open ports.) * bugfix: ``tempo2bpm()`` was broken and returned the wrong value for anything but 500000 microseconds per beat (120 BPM). (Reported and fixed by Jorge Herrera, issue #21) * bugfix: ``merge_tracks()`` didn't work with empty list of tracks. * added proper keyword arguments and doc strings to open functions. 1.1.12 (2014-12-02) ^^^^^^^^^^^^^^^^^^^ * raises IOError if you try to open a virtual port with PortMidi or Pygame. (They are not supported by these backends.) * added ``merge_tracks()``. * removed undocumented method ``MidiFile.get_messages()``. (Replaced by ``merge_tracks(mid.tracks)``.) * bugfix: ``receive()`` checked ``self.callback`` which didn't exist for all ports, causing an ``AttributeError``. 1.1.11 (2014-10-15) ^^^^^^^^^^^^^^^^^^^ * added ``bpm2tempo()`` and ``tempo2bpm()``. * fixed error in documentation (patch by Michael Silver). * added notes about channel numbers to documentation (reported by ludwig404 / leonh, issue #18). 1.1.10 (2014-10-09) ^^^^^^^^^^^^^^^^^^^ * bugfix: MidiFile.length was computer incorrectly. * bugfix: tempo changes caused timing problems in MIDI file playback. (Reported by Michelle Thompson.) * mido-ports now prints port names in single ticks. * MidiFile.__iter__() now yields end_of_track. This means playback will end there instead of at the preceding message. 1.1.9 (2014-10-06) ^^^^^^^^^^^^^^^^^^ * bugfix: _compute_tick_time() was not renamed to _compute_seconds_per_tick() everywhere. * bugfix: sleep time in play() was sometimes negative. 1.1.8 (2014-09-29) ^^^^^^^^^^^^^^^^^^ * bugfix: timing in MIDI playback was broken from 1.1.7 on. Current time was subtracted before time stamps were converted from ticks to seconds, leading to absurdly large delta times. (Reported by Michelle Thompson.) * bugfix: ``read_syx_file()`` didn't handle empty file. 1.1.7 (2014-08-12) ^^^^^^^^^^^^^^^^^^ * some classes and functions have been moved to more accessible locations:: from mido import MidiFile, MidiTrack, MetaMessage from mido.midifiles import MetaSpec, add_meta_spec * you can now iterate over a MIDI file. This will generate all MIDI messages in playback order. The ``time`` attribute of each message is the number of seconds since the last message or the start of the file. (Based on suggestion by trushkin in issue #16.) * added get_sleep_time() to complement set_sleep_time(). * the Backend object no longer looks for the backend module exists on startup, but will instead just import the module when you call one of the ``open_*()`` or ``get_*()`` functions. This test didn't work when the library was packaged in a zip file or executable. This means that Mido can now be installed as Python egg and frozen with tools like PyInstaller and py2exe. See "Freezing Mido Programs" for more on this. (Issue #17 reported by edauenhauer and issue #14 reported by netchose.) * switched to pytest for unit tests. 1.1.6 (2014-06-21) ^^^^^^^^^^^^^^^^^^ * bugfix: package didn't work with easy_install. (Issue #14, reported by netchose.) * bugfix: 100% memory consumption when calling blocking receive() on a PortMidi input. (Issue #15, reported by Francesco Ceruti.) * added wheel support: https://pythonwheels.com/ 1.1.5 (2014-04-18) ^^^^^^^^^^^^^^^^^^ * removed the 'mode' attribute from key_signature messages. Minor keys now have an 'm' appended, for example 'Cm'. * bugfix: sysex was broken in MIDI files. * bugfix: didn't handle MIDI files without track headers. * bugfix: MIDI files didn't handle channel prefix > 15 * bugfix: MIDI files didn't handle SMPTE offset with frames > 29 1.1.4 (2014-10-04) ^^^^^^^^^^^^^^^^^^ * bugfix: files with key signatures Cb, Db and Gb failed due to faulty error handling. * bugfix: when reading some MIDI files Mido crashed with the message "ValueError: attribute must be in range 0..255". The reason was that Meta messages set running status, which caused the next statusless message to be falsely interpreted as a meta message. (Reported by Domino Marama). * fixed a typo in MidiFile._read_track(). Sysex continuation should work now. * rewrote tests to make them more readable. 1.1.3 (2013-10-14) ^^^^^^^^^^^^^^^^^^ * messages are now copied on send. This allows the sender to modify the message and send it to another port while the two ports receive their own personal copies that they can modify without any side effects. 1.1.2 (2013-10-05) ^^^^^^^^^^^^^^^^^^ * bugfix: non-ASCII character caused trouble with installation when LC_ALL=C. (Reported by Gene De Lisa) * bugfix: used old exception handling syntax in rtmidi backend which broke in 3.3 * fixed broken link in 1.1.1 (2013-10-04) ^^^^^^^^^^^^^^^^^^ * bugfix: mido.backends package was not included in distribution. 1.1.0 (2013-10-01) ^^^^^^^^^^^^^^^^^^ * added support for selectable backends (with MIDO_BACKEND) and included python-rtmidi and pygame backends in the official library (as mido.backend.rtmidi and mido.backend.pygame). * added full support for MIDI files (read, write playback) * added MIDI over TCP/IP (socket ports) * added utility programs mido-play, mido-ports, mido-serve and mido-forward. * added support for SMPTE time code quarter frames. * port constructors and ``open_*()`` functions can now take keyword arguments. * output ports now have reset() and panic() methods. * new environment variables MIDO_DEFAULT_INPUT, MIDO_DEFAULT_OUTPUT and MIDO_DEFAULT_IOPORT. If these are set, the open_*() functions will use them instead of the backend's default ports. * added new meta ports MultiPort and EchoPort. * added new examples and updated the old ones. * format_as_string() now takes an include_time argument (defaults to True) so you can leave out the time attribute. * sleep time inside sockets can now be changed. * Message() no longer accepts a status byte as its first argument. (This was only meant to be used internally.) * added callbacks for input ports (PortMidi and python-rtmidi) * PortMidi and pygame input ports now actually block on the device instead of polling and waiting. * removed commas from repr() format of Message and MetaMessage to make them more consistent with other classes. 1.0.4 (2013-08-15) ^^^^^^^^^^^^^^^^^^ * rewrote parser 1.0.3 (2013-07-12) ^^^^^^^^^^^^^^^^^^ * bugfix: __exit__() didn't close port. * changed repr format of message to start with "message". * removed support for undefined messages. (0xf4, 0xf5, 0xf7, 0xf9 and 0xfd.) * default value of velocity is now 64 (0x40). (This is the recommended default for devices that don't support velocity.) 1.0.2 (2013-07-31) ^^^^^^^^^^^^^^^^^^ * fixed some errors in the documentation. 1.0.1 (2013-07-31) ^^^^^^^^^^^^^^^^^^ * multi_receive() and multi_iter_pending() had wrong implementation. They were supposed to yield only messages by default. 1.0.0 (2013-07-20) ^^^^^^^^^^^^^^^^^^ Initial release. Basic functionality: messages, ports and parser. mido-1.3.3/docs/conf.py0000644000175100001770000002224114706731605015514 0ustar runnerdocker00000000000000# # SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen # # SPDX-License-Identifier: CC0-1.0 # # Mido documentation build configuration file, created by # sphinx-quickstart on Wed Jun 26 16:58:08 2013. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import os import sys try: # Python 3.8+ import importlib.metadata as importlib_metadata except ImportError: # v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. html_logo = '../logo/mido.svg' # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None html_base_url = 'https://mido.readthedocs.org/' # Output file base name for HTML help builder. htmlhelp_basename = 'Midodoc' # -- Options for LaTeX output -------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). 'papersize': 'a4paper', # The font size ('10pt', '11pt' or '12pt'). 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ( 'index', 'Mido.tex', 'Mido Documentation', 'Ole Martin Bjørndalen, Raphaël Doursenaud', 'manual' ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. latex_logo = '../logo/mido.png' # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. latex_use_parts = False # If true, show page references after internal links. latex_show_pagerefs = True # If true, show URL addresses after external links. latex_show_urls = 'true' # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ( 'index', 'mido', 'Mido Documentation', ['Ole Martin Bjørndalen', 'Raphaël Doursenaud'], 1 ) ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( 'index', 'Mido', 'Mido Documentation', 'Ole Martin Bjørndalen, Raphaël Doursenaud', 'Mido', 'MIDI Objects for Python', 'Miscellaneous' ), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # -- Options for sphinx.ext.todo ----------------------------------------------- todo_include_todos = True # -- Options for the linkcheck builder ----------------------------------------- linkcheck_ignore = [ r'https://wiki.ccarh.org', # Has aggressive anti-DDoS and will fail often r'https://www.ccarh.org', # Has aggressive anti-DDoS and will fail often r'https://www.sweetwater.com', # Issues 403 for robots r'https://alsa.opensrc.org', # Issues 401 for robots ] mido-1.3.3/docs/contributing.rst0000644000175100001770000002055014706731605017457 0ustar runnerdocker00000000000000.. SPDX-FileCopyrightText: 2016 Ole Martin Bjorndalen .. SPDX-FileCopyrightText: 2023 Raphaël Doursenaud .. .. SPDX-License-Identifier: CC-BY-4.0 Contributing ============ Questions --------- If you have questions about using Mido, contributing code or suggestions for how to make contributing easier, please write at https://github.com/mido/mido/discussions. Bugs & Feature Requests ----------------------- .. note:: If you don't have a precise idea, please use the questions section outlined above instead of opening an issue. If you encounter a bug that is reproducible or want to suggest a new feature - including its implementation details - that would fit the project nicely, feel free to open an issue at https://github.com/mido/mido/issues Please provide as much information as possible to allow us to analyze, including but not limited to: * Operating system name & version * Python version * ``mido`` package version & installation method (Distribution repository, PyPI, source…) * backend used (``amidi``, ``portmidi``, ``rtmidi``, ``PyGame``… Defaults to ``python-rtmidi``.) Forking & Pull Requests ----------------------- The project welcomes all contributions! If you wish to make a change, be it code or documentation, please fork the repository from https://github.com/mido/mido and send your pull request to https://github.com/mido/mido/pulls. Your changes will be reviewed by a maintainer and integrated for publication in the next version of `mido` once approved. Installation ------------ Users ^^^^^ For general usage, see :doc:`installing`. If you wish to install from source, run the following command from the sources root directory:: python3 -m pip install --editable . Or, alternatively if you want to use ports:: python3 -m pip install --editable .[ports-rtmidi] .. note:: *No support* will be provided if you install from source. Developers ^^^^^^^^^^ .. warning:: We recommend that you first setup a *virtual environment* to avoid conflicts with already installed files. .. seealso:: https://packaging.python.org/en/latest/tutorials/installing-packages/ Then, to install the *development dependencies*, you can run the following command from inside your virtual environment:: python3 -m pip install --editable .[dev] Or, alternatively, if you want to use ports:: python3 -m pip install --editable .[dev,ports-rtmidi] This will install all needed dependencies for linting, testing, documentation generation and publishing releases. Code Checks ----------- .. note:: The following code checks are done automatically using a GitHub Actions Workflow (Defined in :file:`.github/workflow/tests.yml`) for each push to the ``main`` branch and each Pull Request. It's good practice to check your changes *locally* before submitting. Linting ^^^^^^^ Linting is done with `ruff `_. Its configuration can be found in `pyproject.toml`. You can lint your code using:: ruff check . Copyright and REUSE Compliance ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The project is `REUSE `_ compliant. If you wish to add your copyright to a file, please add an SPDX header if the form of: .. code-block:: python # SPDX-FileCopyrightText: YYYY First_Name Last_Name # # SPDX-License-Identifier: MIT .. note:: Use the appropriate comment format and license for the file and only add the first line below existing copyright mentions if modifying an existing file. The year should only be set the first time you edit a file and never touched again. There is **no** benefit in updating it constantly! then run:: reuse lint Testing ^^^^^^^ `pytest `_ is used for unit testing. The tests are found in `tests/test_*.py <../tests/>`_. The default configuration is declared in the ``tool.pytest.ini_options`` section of :file:`pyproject.toml`. The test suite can be run using the command:: pytest Checking the Release Manifest ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ To make sure the repository and source code manifest (:file:`.MANIFEST.in`) are in sync:: check-manifest --verbose Building the Documentation ^^^^^^^^^^^^^^^^^^^^^^^^^^ The documentation is generated using `Sphinx `_. To generate the HTML documentation:: sphinx-build -j auto -q -W -E --keep-going docs docs/_build If you wish to build a PDF version for *local* use: #. Install a `LaTeX `_ distribution #. Install `ImageMagick `_ #. use:: sphinx-build -M latexpdf docs docs/_build You'll find the resulting PDF file at :file:`docs/_build/latex/Mido.pdf`. Once generated and copied in a safe place, you may want to remove the build artifacts:: sphinx-build -M clean docs docs/_build Testing MIDI File Support ------------------------- Test Files ^^^^^^^^^^ The `Lakh MIDI Dataset `_ is a great resource for testing the MIDI file parser. Releasing --------- The processes are now automated. .. note:: The whole team has access to manual publishing to :term:`PyPI` and :term:`Read the Docs` in case of automation defect. Documentation ^^^^^^^^^^^^^ To generate the official documentation, we use :term:`Read the Docs` integration services for GitHub. Every time a new commit is pushed or merged onto our ``main`` development branch on GitHub, the ``latest`` version of the documentation is updated by Read the Docs. Each time a new version is tagged, the new documentation version is created, built, published and eventually promoted to ``stable`` following Semantic Versioning. The ``stable`` version of the documentation is the one served by default if no specific version is chosen. We also build a mirror of the current ``main`` development branch documentation using a GitHub Workflow and hosted on GitHub pages. All of this is defined by :file:`.github/workflow/documentation.yml` Package ^^^^^^^ The process uses GitHub Action Workflow defined by :file:`.github/workflow/release.yml` and is triggered upon receiving a tag. Preparation ^^^^^^^^^^^ Make sure all the tests pass, documentation has been updated and everything is in good order before proceeding. .. note:: The version number should be :pep:`440` & SemVer compliant. ``X.Y.Z`` is the version, for example ``1.1.18`` or ``1.2.0``. #. update the changelog in :file:`docs/changes.rst`. The following commands may prove useful to retrieve all Pull Requests & all commits:: previous_release_tag=git describe --abbrev=0 git log --oneline --merges --reverse "${previous_release_tag}.." git log --oneline --no-merges --reverse "${previous_release_tag}.." #. update version and date in :file:`docs/changes.rst` #. commit the changes:: git commit -a -c "Prepare release." #. set the version number by tagging the release:: git tag -a -m "mido version " .. note:: We use an annotated tag here to retain all information about the tagger and create a proper object in the GIT database instead of a commit alias. .. seealso:: https://git-scm.com/book/en/v2/Git-Basics-Tagging #. don’t forget to push your changes including the tags to GitHub to trigger the auto-release process:: git push --tags Manual steps (Recovery) ^^^^^^^^^^^^^^^^^^^^^^^ .. warning:: Only use if the automatic process fails for some reason. #. Prepare a clean environment:: git clone --branch --single-branch https://github.com/mido/mido mido- cd mido- python3 -m venv mido-build #. Build:: source mido-build/bin/activate python3 -m pip install --upgrade pip setuptools wheel build twine python3 -m build #. Publish on Test PyPI:: python3 -m build twine upload --repository testpypi dist/* #. Check that the published package is good:: python3 -m pip install --index-url https://test.pypi.org/simple/ --no-deps mido python3 -c "import mido; print(mido.version_info)" .. todo:: Now would be a good time to run some integration tests once we have them. #. Publish on PyPI:: twine upload dist/* .. warning:: This is the most critical step of the process. This **cannot** be undone. Make sure everything is in good order before pressing the "big red button"! mido-1.3.3/docs/files/0000755000175100001770000000000014706731615015317 5ustar runnerdocker00000000000000mido-1.3.3/docs/files/index.rst0000644000175100001770000000024014706731605017153 0ustar runnerdocker00000000000000.. SPDX-FileCopyrightText: 2023 Raphaël Doursenaud .. .. SPDX-License-Identifier: CC-BY-4.0 Files ===== .. toctree:: midi syx mido-1.3.3/docs/files/midi.rst0000644000175100001770000001756014706731605017003 0ustar runnerdocker00000000000000.. SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen .. .. SPDX-License-Identifier: CC-BY-4.0 Standard MIDI Files =================== ``MidiFile`` objects can be used to *read*, *write* and *play back* MIDI files. Opening ------- You can open a file with:: from mido import MidiFile mid = MidiFile('song.mid') .. note:: :term:`SysEx` dumps such as patch data are often stored in ``SYX`` files rather than MIDI files. If you get "MThd not found. Probably not a MIDI file" try ``mido.read_syx_file()``. (See :doc:`syx` for more.) The ``tracks`` attribute is a list of tracks. Each track is a list of messages and meta messages, with the ``time`` attribute of each messages set to its delta time (in ticks). (See Tempo and Beat Resolution below for more on delta times.) To print out all messages in the file, you can do:: for i, track in enumerate(mid.tracks): print('Track {}: {}'.format(i, track.name)) for msg in track: print(msg) The entire file is read into memory. Thus you can freely modify tracks and messages and save the file back by calling the ``save()`` method. (More on this below.) Iterating Over Messages ----------------------- Iterating over a ``MidiFile`` object will generate all MIDI messages in the file in playback order. The ``time`` attribute of each message is the number of seconds since the last message or the start of the file. Meta messages will also be included. If you want to filter them out, you can do:: if msg.is_meta: ... This makes it easy to play back a MIDI file on a port (though this simple implementation is subject to time drift):: for msg in MidiFile('song.mid'): time.sleep(msg.time) if not msg.is_meta: port.send(msg) This is so useful that there's a method for it:: for msg in MidiFile('song.mid').play(): port.send(msg) This does the sleeping and filtering for you (while avoiding drift). If you pass ``meta_messages=True`` you will also get meta messages. These **cannot** be sent on ports, which is why they are ``off`` by default. Creating a New File ------------------- You can create a new file by calling ``MidiFile`` without the ``filename`` argument. The file can then be saved by calling the ``save()`` method:: from mido import Message, MidiFile, MidiTrack mid = MidiFile() track = MidiTrack() mid.tracks.append(track) track.append(Message('program_change', program=12, time=0)) track.append(Message('note_on', note=64, velocity=64, time=32)) track.append(Message('note_off', note=64, velocity=127, time=32)) mid.save('new_song.mid') The ``MidiTrack`` class is a subclass of list, so you can use all the usual methods. All messages must be tagged with delta time (in ticks). (A delta time is how long to wait before the next message.) If there is no ``end_of_track`` message at the end of a track, one will be written anyway. A complete example can be found in ``examples/midifiles/``. The ``save`` method takes either a filename (``str``) or, using the ``file`` keyword parameter, a file-like object such as an in-memory binary file (an ``io.BytesIO``). If you pass a file object, ``save`` does not close it. Similarly, the ``MidiFile`` constructor can take either a filename, or a file object by using the ``file`` keyword parameter. if you pass a file object to ``MidiFile`` as a context manager, the file is not closed when the context manager exits. Examples can be found in ``test_midifiles2.py``. File Types ---------- There are three types of MIDI files: * type 0 (single track): all messages are saved in one track * type 1 (synchronous): all tracks start at the same time * type 2 (asynchronous): each track is independent of the others When creating a new file, you can select type by passing the ``type`` keyword argument or by setting the ``type`` attribute:: mid = MidiFile(type=2) mid.type = 1 Type 0 files must have exactly one track. A ``ValueError`` is raised if you attempt to save a file with no tracks or with more than one track. Playback Length --------------- You can get the total playback time in seconds by accessing the ``length`` property:: mid.length This is only supported for type 0 and 1 files. Accessing ``length`` on a type 2 file will raise ``ValueError``, since it is impossible to compute the playback time of an asynchronous file. Meta Messages ------------- Meta messages behave like normal messages and can be created in the usual way, for example:: >>> from mido import MetaMessage >>> MetaMessage('key_signature', key='C#', mode='major') MetaMessage('key_signature', key='C#', mode='major', time=0) You can tell meta messages apart from normal messages with:: if msg.is_meta: ... or if you know the message type you can use the ``type`` attribute:: if msg.type == 'key_signature': ... elif msg.type == 'note_on': ... Meta messages **cannot** be sent on ports. For a list of supported meta messages and their attributes, and also how to implement new meta messages, see :doc:`../meta_message_types`. About the Time Attribute ------------------------ The ``time`` attribute is used in several different ways: * inside a track, it is delta time in ticks. This must be an integer. * in messages yielded from ``play()``, it is delta time in seconds (time elapsed since the last yielded message) * (only important to implementers) inside certain methods it is used for absolute time in ticks or seconds .. todo: Review implementation to separate concerns and units into dedicated attributes. Tempo and Time Resolution ------------------------- .. image:: ../images/midi_time.svg Timing in MIDI files is centered around ticks. Each message in a MIDI file has a delta time, which tells how many ticks have passed since the last message. A tick is the smallest unit of time in MIDI and remains fixed throughout the song. Each quarter notes is divided into a certain number of ticks, often referred as the resolution of the file or pulses per quarter note (PPQN). This resolution is stored as ``ticks_per_beat`` in MidiFile objects. The meaning of this ``ticks_per_beat`` in terms of absolute timing depends on the tempo and time signature of the file. MIDI Tempo vs. BPM ^^^^^^^^^^^^^^^^^^ Unlike music, tempo in MIDI is not given as beats per minute (BPM), but rather in microseconds per quarter note, with a default tempo of 500000 microseconds per quarter note. Given a default 4/4 time signature where a beat is exactly a quarter note, this corresponds to 120 beats per minute. In case of different time signatures, the length of a beat depends on the denominator of the time signature. E.g. in 2/2 time signature a beat has a length of a half note, i.e. two quarter notes. Thus the default MIDI tempo of 500000 corresponds to a beat length of 1 second which is 60 BPM. The meta messages 'set_tempo' and 'time_signature' can be used to change the tempo and time signature during a song, respectively. You can use :py:func:`bpm2tempo` and :py:func:`tempo2bpm` to convert to and from beats per minute. Note that :py:func:`tempo2bpm` may return a floating point number. Converting Between Ticks and Seconds ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ To convert from MIDI time to absolute time in seconds, the tempo (either in number of beats per minute (BPM) or microseconds per quarter note, see `MIDI Tempo vs. BPM`_ above) and ticks per per quarter note have to be decided upon. You can use :py:func:`tick2second` and :py:func:`second2tick` to convert to and from seconds and ticks. Note that integer rounding of the result might be necessary because MIDI files require ticks to be integers. If you have a lot of rounding errors you should increase the time resolution with more ticks per quarter note, by setting MidiFile.ticks_per_beat to a large number. Typical values range from 96 to 480 but some use even more ticks per quarter note. mido-1.3.3/docs/files/syx.rst0000644000175100001770000000175314706731605016701 0ustar runnerdocker00000000000000.. SPDX-FileCopyrightText: 2014 Ole Martin Bjorndalen .. .. SPDX-License-Identifier: CC-BY-4.0 SYX Files ========= SYX files are used to store :term:`SysEx` messages, usually for patch data. Reading and Writing ------------------- To read a ``SYX`` file:: messages = mido.read_syx_file('patch.syx') To write a ``SYX`` file:: mido.write_syx_file('patch.syx', messages) Non-sysex messages will be ignored. Plain Text Format ----------------- Mido also supports *plain text* SYX files. These are read in exactly the same way:: messages = mido.read_syx_file('patch.txt') ``read_syx_file()`` determines which format the file is by looking at the first byte. It raises ``ValueError`` if file is plain text and byte is not a 2-digit hex number. To write plain text:: mido.write_syx_file('patch.txt', messages, plaintext=True) This will write the messages as hex encoded bytes with one message per line:: F0 00 01 5D 02 00 F7 F0 00 01 5D 03 00 F7 mido-1.3.3/docs/freezing_exe.rst0000644000175100001770000000240214706731605017416 0ustar runnerdocker00000000000000.. SPDX-FileCopyrightText: 2014 Ole Martin Bjorndalen .. .. SPDX-License-Identifier: CC-BY-4.0 Freezing to EXE File ==================== PyInstaller ----------- When you build an executable with PyInstaller and run it you may get import errors like this one:: ImportError: No module named mido.backends.portmidi The reason is that Mido uses ``import_module()`` to import the backend modules, while PyInstaller looks for ``import`` statements. The easiest fix is to import the module at the top of the program:: import mido import mido.backends.portmidi # The backend you want to use. print(mido.get_input_names()) and then run ``pyinstaller`` like usual:: $ pyinstaller --onefile midotest.py $ ./dist/midotest [u'Midi Through Port-0'] If you don't want to change the program, you can instead declare the backend module as a `hidden import `_. bbFreeze, py2exe, cx_Freeze, py2app, etc. ----------------------------------------- I suspect the same is true for these, but I have not had a chance to try it out yet. Adding the explicit ``import`` statement should always work, though, since Mido backends are just normal Python modules. mido-1.3.3/docs/glossary.rst0000644000175100001770000000431414706731605016613 0ustar runnerdocker00000000000000.. SPDX-FileCopyrightText: 2023 Raphaël Doursenaud .. .. SPDX-License-Identifier: CC-BY-4.0 Glossary ======== .. glossary:: ascii American Standard Code for Information Interchange. The most popular character encoding standard. backend backends backend(s) A Mido backend is the interface between the library and the operating system level MIDI stack. See :doc:`backends/index` for more informations. callback A function called by the :term:`backend` when message(s) are ready to process. cli Command Line Interface. file files midi file standard midi file SMF A standard MIDI file. As defined by the MIDI Association's specification. message messages A MIDI message. midi The Musical Instrument Digital Interface. The specification is maintained by the `MIDI Association `_. nibble Half a byte (usually 4 bits). An 8-bit byte has 2 nibbles: an upper and a lower nibble. pip The `Python Package Installer `_. port ports A MIDI port. pypi The `Python Package Index `_. python The `Python programming language `_. rtd read the docs `Read the Docs `_ or RTD for short is a popular service to build, manage versions and host documentation generated from Sphinx (and now MkDocs) in the Python ecosystem. rtpmidi A standard protocol to send MIDI over a TCP/IP link. .. seealso:: * :rfc:`4695` * :rfc:`4696` tcp Transmission Control Protocol. .. seealso:: :rfc:`9293` tick ticks The :term:`MIDI File` unit of time. sysex system exclusive Special :term:`MIDI` messages that are intended for consumption by a specific device. Details about the structure and meaning of these messages are often found in the device's manual. .. todo:: Fill this glossary and add the ``:term:`` directive where appropriate. mido-1.3.3/docs/images/0000755000175100001770000000000014706731615015462 5ustar runnerdocker00000000000000mido-1.3.3/docs/images/midi_time.svg0000644000175100001770000004754714706731605020163 0ustar runnerdocker00000000000000 image/svg+xml Minutes Beats Ticks 4 beats per minute (BPM) 3 ticks per beat mido-1.3.3/docs/images/midi_time.svg.license0000644000175100001770000000015414706731605021563 0ustar runnerdocker00000000000000SPDX-FileCopyrightText: 2016 Ole Martin Bjorndalen SPDX-License-Identifier: CC-BY-4.0 mido-1.3.3/docs/index.rst0000644000175100001770000000675314706731605016070 0ustar runnerdocker00000000000000.. SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen .. SPDX-FileCopyrightText: 2023 Raphaël Doursenaud .. .. SPDX-License-Identifier: CC-BY-4.0 .. Mido documentation main file, created by sphinx-quickstart on Wed Jun 26 16:58:08 2013. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. .. include:: shared/licenses_logos.rst Mido - MIDI Objects for Python ============================== .. Main TOC to get a proper title in the PDF: .. toctree:: :caption: Table of Contents :maxdepth: 4 .. Workaround to properly show parts in both PDF (latex) and HTML (ReadTheDocs) While awaiting a resolution of https://github.com/sphinx-doc/sphinx/issues/4977 .. raw:: latex \part{Introduction} .. toctree:: :caption: Introduction :maxdepth: 3 :hidden: self Overview -------- Mido is a :term:`Python` library for working with :term:`MIDI` 1.0 :term:`ports`, :term:`messages` and :term:`files`: .. code-block:: python >>> import mido >>> msg = mido.Message('note_on', note=60) >>> msg.type 'note_on' >>> msg.note 60 >>> msg.bytes() [144, 60, 64] >>> msg.copy(channel=2) Message('note_on', channel=2, note=60, velocity=64, time=0) .. code-block:: python port = mido.open_output('Port Name') port.send(msg) .. code-block:: python with mido.open_input() as inport: for msg in inport: print(msg) .. code-block:: python mid = mido.MidiFile('song.mid') for msg in mid.play(): port.send(msg) Mido is short for *MIDI objects*. About this document ^^^^^^^^^^^^^^^^^^^ .. version is automatically generated. This document refers to Mido version |release|. .. note:: An up-to-date version of this document is always available at https://mido.readthedocs.io. .. Build instructions have been moved to contributing. License ^^^^^^^ This documentation (Except our code of conduct) is licensed under the `Creative Commons Attribution 4.0 International License `__. |Creative Commons BY-4.0 License| .. seealso:: :doc:`licenses` Community & Source Code ^^^^^^^^^^^^^^^^^^^^^^^ Come visit us at https://github.com/mido. Everybody is welcome! .. seealso:: * :doc:`CODE_OF_CONDUCT` * :doc:`contributing` .. Indices are systematically generated for the PDF. Wrap into an HTML only section to prevent spurious title in the TOC. .. only:: html Tables and indices ------------------ * :ref:`genindex` * :ref:`modindex` .. Comment since Search is provided by the ReadTheDocs theme. .. * :ref:`search` .. The rest of the TOC and documents: .. raw:: latex \part{Basics} .. toctree:: :caption: Basics :maxdepth: 3 :hidden: installing intro .. raw:: latex \part{Details} .. toctree:: :caption: Details :maxdepth: 3 :hidden: messages/index backends/index ports/index files/index binaries .. raw:: latex \part{Reference} .. toctree:: :caption: Reference :maxdepth: 3 :hidden: api .. raw:: latex \part{Community} .. toctree:: :caption: Community :maxdepth: 3 :hidden: CODE_OF_CONDUCT contributing .. raw:: latex \part{Appendix} .. toctree:: :caption: Appendix :maxdepth: 3 :hidden: about_midi message_types meta_message_types resources freezing_exe changes authors licenses acknowledgements glossary mido-1.3.3/docs/installing.rst0000644000175100001770000000257114706731605017117 0ustar runnerdocker00000000000000.. SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen .. SPDX-FileCopyrightText: 2023 Raphaël Doursenaud .. .. SPDX-License-Identifier: CC-BY-4.0 Installing ========== Requirements ------------ Mido requires :term:`Python` version 3.7 or higher. A few dependencies are also required in order to allow Mido to introspect its own version: * `packaging` * `importlib_metadata` for :term:`Python` < 3.8 .. note:: Dependency management is handled automatically when installing using the recommended methods. No need to bother installing these manually. Optional -------- Dependencies for the loaded on-demand :term:`port` :term:`backend(s)` are optional unless you want to use the :term:`ports` feature. See :doc:`backends/index` for help choosing a :term:`backend`. Installation ------------ The recommended installation method is to use :term:`pip` to retrieve the package from :term:`PyPi`. .. note:: Consider using a *virtual environment* to isolate your installation from your current environment. This ensures that you always get the latest released stable version:: python3 -m pip install mido Or, alternatively, if you want to use :term:`ports` with the default :term:`backend`:: python3 -m pip install mido[ports-rtmidi] See :doc:`backends/index` for installation instructions for other :term:`backends`. mido-1.3.3/docs/intro.rst0000644000175100001770000001220714706731605016103 0ustar runnerdocker00000000000000.. SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen .. .. SPDX-License-Identifier: CC-BY-4.0 Introduction (Basic Concepts) ============================= Mido is all about messages, ports and files. Messages -------- Mido allows you to work with MIDI messages as Python objects. To create a new message:: >>> from mido import Message >>> msg = Message('note_on', note=60) >>> msg Message('note_on', channel=0, note=60, velocity=64, time=0) .. note:: Mido numbers channels ``0`` to ``15`` instead of ``1`` to ``16``. This makes them easier to work with from Python but you may want to add and subtract ``1`` when communicating with the user. A list of all supported message types and their parameters can be found in :doc:`message_types`. The values can now be accessed as attributes:: >>> msg.type 'note_on' >>> msg.note 60 >>> msg.velocity 64 Attributes are also settable but this should be avoided. It's better to use ``msg.copy()``:: >>> msg.copy(note=100, velocity=127) Message('note_on', channel=0, note=100, velocity=127, time=0) Type and value checks are done when you pass parameters or assign to attributes, and the appropriate exceptions are raised. This ensures that the message is always valid. For more about messages, see :doc:`messages/index`. Type and Value Checking ^^^^^^^^^^^^^^^^^^^^^^^ Mido messages come with type and value checking built in:: >>> import mido >>> mido.Message('note_on', channel=2092389483249829834) Traceback (most recent call last): File "", line 1, in File "/home/olemb/src/mido/mido/messages/messages.py", line 89, in __init__ check_msgdict(msgdict) File "/home/olemb/src/mido/mido/messages/checks.py", line 100, in check_msgdict check_value(name, value) File "/home/olemb/src/mido/mido/messages/checks.py", line 87, in check_value _CHECKS[name](value) File "/home/olemb/src/mido/mido/messages/checks.py", line 17, in check_channel raise ValueError('channel must be in range 0..15') ValueError: channel must be in range 0..15 This means that the message object is always a valid MIDI message. Ports ----- To create an output port and send a message:: >>> outport = mido.open_output() >>> outport.send(msg) To create an input port and receive a message:: >>> inport = mido.open_input() >>> msg = inport.receive() .. note:: Multiple threads can safely send and receive notes on the same port. This will give you the default output and input ports. If you want to open a specific port, you will need its name. To get a list of all available input ports:: >>> mido.get_input_names() ['Midi Through Port-0', 'SH-201', 'Integra-7'] >>> inport = mido.open_input('SH-201') All Mido ports can be used with the ``with`` statement, which will close the port for you:: with mido.open_input('SH-201') as inport: ... To iterate through all incoming messages:: for msg in inport: ... You can also receive and iterate over messages in a non-blocking way. For more about ports, see :doc:`ports/index`. All Ports are Ports ^^^^^^^^^^^^^^^^^^^ The input and output ports used above are device ports, which communicate with a physical or virtual MIDI device. Other port types include: * ``MultiPort``, which wraps around a set of ports and allows you to send to all of them or receive from all of them as if they were one. * ``SocketPort``, which communicates with another port over a TCP/IP (network) connection. * ``IOPort``, which wraps around an input and an output port and allows you to send and receive messages as if the two were the same port. Ports of all types look and behave the same way, so they can be used interchangeably. It's easy to write new port types. See :doc:`ports/custom`. Virtual Ports ^^^^^^^^^^^^^ Virtual ports allows you to create new ports that other applications can connect to:: with mido.open_input('New Port', virtual=True) as inport: for message in inport: print(message) The port should now appear to other applications as "``New Port``". .. warning:: Unfortunately virtual ports are not supported by PortMidi and Pygame so this only works with RtMidi. Furthermore, RtMidi's virtual ports are not available under Microsoft Windows. See: :doc:`backends/rtmidi` for details. Raw MIDI Bytes Parser --------------------- Mido comes with a parser that allows you to turn ``bytes`` into messages. You can create a new parser:: >>> p = mido.Parser() >>> p.feed([0x90, 0x40]) >>> p.feed_byte(0x60) You can then fetch messages out of the parser:: >>> p.pending() 1 >>> for message in p: ... print(message) ... note_on channel=0 note=64 velocity=96 time=0 For more on parsers and parsing see :doc:`messages/parsing`. .. versionadded:: 1.2 You can also create a message from ``bytes`` using class methods: .. code-block:: python msg1 = mido.Message.from_bytes([0x90, 0x40, 0x60]) msg2 = mido.Message.from_hex('90, 40 60') The ``bytes`` must contain exactly one complete message. If not ``ValueError`` is raised. mido-1.3.3/docs/licenses.rst0000644000175100001770000000173014706731605016554 0ustar runnerdocker00000000000000.. SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen .. SPDX-FileCopyrightText: 2023 Raphaël Doursenaud .. .. SPDX-License-Identifier: CC-BY-4.0 .. include:: shared/licenses_logos.rst Licenses ======== .. code-block:: text Copyright (C) 2013 Ole Martin Bjørndalen Copyright (C) 2023 Raphaël Doursenaud Source Code ----------- Mido is published under the terms of the :download:`MIT License (MIT) <../LICENSES/MIT.txt>`. Project configuration --------------------- :download:`CC0 1.0 Universal <../LICENSES/CC0-1.0.txt>`. |Creative Commons CC0-1.0 License| Documentation, Illustrations & Logo ----------------------------------- :download:`Creative Commons Attribution 4.0 International <../LICENSES/CC-BY-4.0.txt>`. |Creative Commons BY-4.0 License| Code of Conduct --------------- :download:`Creative Commons Attribution-ShareAlike 3.0 <../LICENSES/CC-BY-SA-3.0.txt>`. |Creative Commons BY-SA-3.0 License| mido-1.3.3/docs/make.bat0000644000175100001770000001213214706731605015620 0ustar runnerdocker00000000000000REM SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen REM REM SPDX-License-Identifier: CC0-1.0 @ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Mido.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Mido.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end mido-1.3.3/docs/message_types.rst0000644000175100001770000000443014706731605017617 0ustar runnerdocker00000000000000.. SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen .. .. SPDX-License-Identifier: CC-BY-4.0 Message Types ============= Supported Messages ------------------ ============== ============================== Name Keyword Arguments / Attributes ============== ============================== note_off channel note velocity note_on channel note velocity polytouch channel note value control_change channel control value program_change channel program aftertouch channel value pitchwheel channel pitch sysex data quarter_frame frame_type frame_value songpos pos song_select song tune_request clock start continue stop active_sensing reset ============== ============================== ``quarter_frame`` is used for SMPTE time codes. Parameter Types --------------- =========== ====================== ================ Name Valid Range Default Value =========== ====================== ================ channel 0..15 0 frame_type 0..7 0 frame_value 0..15 0 control 0..127 0 note 0..127 0 program 0..127 0 song 0..127 0 value 0..127 0 velocity 0..127 64 data (0..127, 0..127, ...) () (empty tuple) pitch -8192..8191 0 pos 0..16383 0 time any integer or float 0 =========== ====================== ================ .. note:: Mido numbers channels 0 to 15 instead of 1 to 16. This makes them easier to work with in Python but you may want to add and subtract 1 when communicating with the user. ``velocity`` is how fast the note was struck or released. It defaults to 64 so that if you don't set it, you will still get a reasonable value. (64 is the recommended default for devices that don't support it attack or release velocity.) The ``time`` is used in MIDI files as delta time. The ``data`` parameter accepts any iterable that generates numbers in 0..127. This includes:: mido.Message('sysex', data=[1, 2, 3]) mido.Message('sysex', data=range(10)) mido.Message('sysex', data=(i for i in range(10) if i % 2 == 0)) mido-1.3.3/docs/messages/0000755000175100001770000000000014706731615016024 5ustar runnerdocker00000000000000mido-1.3.3/docs/messages/frozen.rst0000644000175100001770000000261214706731605020061 0ustar runnerdocker00000000000000.. SPDX-FileCopyrightText: 2017 Ole Martin Bjorndalen .. .. SPDX-License-Identifier: CC-BY-4.0 Frozen Messages --------------- .. versionadded:: 1.2 Since Mido messages are *mutable* (can change) they can not be hashed or put in dictionaries. This makes it hard to use them for things like Markov chains. In these situations you can use *frozen messages*: .. code-block:: python from mido.frozen import FrozenMessage msg = FrozenMessage('note_on') d = {msg: 'interesting'} *Frozen messages* are used and behave in exactly the same way as normal messages with one exception: **attributes are not settable**. There are also variants for meta messages (``FrozenMetaMessage`` and ``FrozenUnknownMetaMessage``). You can *freeze* and *thaw* messages with: .. code-block:: python from mido.frozen import freeze_message, thaw_message frozen = freeze_message(msg) thawed = thaw_message(frozen) ``thaw_message()`` will always return a *copy*. Passing a *frozen message* to ``freeze_message()`` will return the original message. Both functions return ``None`` if you pass ``None`` which is handy for things like: .. code-block:: python msg = freeze_message(port.receive()) for msg in map(freeze_message, port): ... To check if a message is *frozen*: .. code-block:: python from mido.frozen import is_frozen if is_frozen(msg): ... mido-1.3.3/docs/messages/index.rst0000644000175100001770000001104314706731605017663 0ustar runnerdocker00000000000000.. SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen .. .. SPDX-License-Identifier: CC-BY-4.0 Messages ======== A Mido message is a Python object with methods and attributes. The attributes will vary depending on message type. To create a new message:: >>> mido.Message('note_on') Message('note_on', channel=0, note=0, velocity=64, time=0) You can pass attributes as keyword arguments:: >>> mido.Message('note_on', note=100, velocity=3, time=6.2) Message('note_on', channel=0, note=100, velocity=3, time=6.2) All attributes will default to ``0``. The exceptions are ``velocity``, which defaults to ``64`` (middle velocity) and ``data`` which defaults to ``()``. You can set and get attributes as you would expect:: >>> msg = mido.Message('note_on') >>> msg.note 0 The ``type`` attribute can be used to determine message type:: >>> msg.type 'note_on' Attributes are also settable but it's always better to use ``msg.copy()``:: >>> msg.copy(note=99, time=100.0) Message('note_on', channel=0, note=99, velocity=64, time=100.0) .. note:: Mido always makes a copy of messages instead of modifying them so if you do the same you have immutable messages in practice. (Third party libraries may not follow the same rule.) .. note:: :doc:`frozen` are a variant of messages that are hashable and can be used as dictionary keys. They are also safe from tampering by third party libraries. You can freely convert between the two and use frozen messages wherever normal messages are allowed. Mido supports all message types defined by the :term:`MIDI` standard. For a full list of messages and their attributes, see :doc:`../message_types`. Control Changes --------------- .. code-block:: python if msg.is_cc(): print('Control change message received') if msg.is_cc(7): print('Volume changed to', msg.value) Converting To & From Bytes -------------------------- To Bytes ^^^^^^^^ You can convert a message to :term:`MIDI` ``bytes`` with one of these methods: >>> msg = mido.Message('note_on') >>> msg Message('note_on', channel=0, note=0, velocity=64, time=0) >>> msg.bytes() [144, 0, 64] >>> msg.bin() bytearray(b'\x90\x00@') >>> msg.hex() '90 00 40' From Bytes ^^^^^^^^^^ You can turn ``bytes`` back into messages with the :doc:`parser `. .. versionadded:: 1.2 You can also create a message from ``bytes`` using class methods: .. code-block:: python msg1 = mido.Message.from_bytes([0x90, 0x40, 0x60]) msg2 = mido.Message.from_hex('90, 40 60') The ``bytes`` must contain exactly one complete message. If not ``ValueError`` is raised. The Time Attribute ------------------ Each message has a ``time`` attribute, which can be set to any value of type ``int`` or ``float``. Some parts of Mido use the attribute for special purposes. In ``MIDI file`` tracks, it is used as delta time (in :term:`ticks`), and it must be a non-negative integer. In other parts of Mido, this value is ignored. .. versionchanged:: 1.1.18 In earlier versions, the ``time`` attribute was not included in comparisons. If you want the old behavior the easiest way is ``msg1.bytes() == msg2.bytes()``. To sort messages on time you can do:: messages.sort(key=lambda message: message.time) or:: import operator messages.sort(key=operator.attrgetter('time')) System Exclusive Messages ------------------------- :term:`System Exclusive` (aka :term:`SysEx`) messages are used to send device specific data. The ``data`` attribute is a tuple of data bytes which serves as the payload of the message:: >>> msg = Message('sysex', data=[1, 2, 3]) >>> msg Message('sysex', data=(1, 2, 3), time=0) >>> msg.hex() 'F0 01 02 03 F7' You can also extend the existing data:: >>> msg = Message('sysex', data=[1, 2, 3]) >>> msg.data += [4, 5] >>> msg.data += [6, 7, 8] >>> msg Message('sysex', data=(1, 2, 3, 4, 5, 6, 7, 8), time=0) Any sequence of integers between `0` and `127` is allowed, and type and range checking is applied to each data byte. These are all valid:: (65, 66, 67) [65, 66, 67] (i + 65 for i in range(3)) (ord(c) for c in 'ABC') bytearray(b'ABC') b'ABC' # Python 3 only. For example:: >>> msg = Message('sysex', data=bytearray(b'ABC')) >>> msg.data += bytearray(b'DEF') >>> msg Message('sysex', data=(65, 66, 67, 68, 69, 70), time=0) .. include:: frozen.rst .. include:: parsing.rst .. include:: serializing.rst mido-1.3.3/docs/messages/parsing.rst0000644000175100001770000000447614706731605020233 0ustar runnerdocker00000000000000.. SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen .. .. SPDX-License-Identifier: CC-BY-4.0 Parsing MIDI Bytes ------------------ The MIDI protocol is a *binary protocol*. Each message is encoded as a *status* byte followed by up to three *data* bytes. (Except :term:`SysEx` messages which can have an arbitrary number of *data* bytes immediately followed by an EOX status byte.) .. versionadded:: 1.2 ``mido.Message.from_hex()`` .. note:: To parse a single message you can use the class methods ``mido.Message.from_bytes()`` and ``mido.Message.from_hex()`` Mido comes with a *parser* that turns MIDI bytes into messages. You can create a *parser object* or call one of the *utility functions*:: >>> mido.parse([0x92, 0x10, 0x20]) Message('note_on', channel=2, note=16, velocity=32, time=0) >>> mido.parse_all([0x92, 0x10, 0x20, 0x82, 0x10, 0x20]) [Message('note_on', channel=2, note=16, velocity=32, time=0), Message('note_off', channel=2, note=16, velocity=32, time=0)] These functions are just shortcuts for the full ``Parser`` class. This is the same parser as used inside input ports to parse incoming messages. Here are a few examples of how it can be used:: >>> p = mido.Parser() >>> p.feed([0x90, 0x10, 0x20]) >>> p.pending() 1 >>> p.get_message() Message('note_on', channel=0, note=16, velocity=32, time=0) >>> p.feed_byte(0x90) >>> p.feed_byte(0x10) >>> p.feed_byte(0x20) >>> p.feed([0x80, 0x10, 0x20]) >>> p.pending() 2 >>> p.get_message() Message('note_on', channel=0, note=16, velocity=32, time=0) >>> p.get_message() Message('note_off', channel=0, note=16, velocity=32, time=0) ``feed()`` accepts any iterable that generates integers in 0..255. The parser will skip and stray status bytes or data bytes, so you can safely feed it random data and see what comes out the other end. ``get_message()`` will return ``None`` if there are no messages ready to be gotten. You can also fetch parsed messages out of the parser by iterating over it:: >>> p.feed([0x92, 0x10, 0x20, 0x82, 0x10, 0x20]) >>> for message in p: ... print(message) note_on channel=2 note=16 velocity=32 time=0 note_off channel=2 note=16 velocity=32 time=0 The messages are available in ``p.messages`` (a ``collections.deque``). mido-1.3.3/docs/messages/serializing.rst0000644000175100001770000000657214706731605021107 0ustar runnerdocker00000000000000.. SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen .. .. SPDX-License-Identifier: CC-BY-4.0 Serializing ----------- String Encoding ^^^^^^^^^^^^^^^ Mido messages can be serialized to a text format, which can be used to safely store messages in text files, send them across sockets or embed them in JSON, among other things. To *encode* a message, simply call ``str()`` on it:: >>> cc = control_change(channel=9, control=1, value=122, time=60) >>> str(cc) 'control_change channel=9 control=1 value=122 time=60' To convert the other way (new method in 1.2):: >>> mido.Message.from_str('control_change control=1 value=122') Message('control_change', channel=0, control=1, value=122, time=0) Alternatively, you can call the ``format_as_string`` function directly: >>> mido.format_as_string(cc) 'control_change channel=9 control=1 value=122 time=60' If you don't need the time attribute or you want to store it elsewhere, you can pass ``include_time=False``:: >>> mido.format_as_string(cc) 'control_change channel=9 control=1 value=122' (This option is also available in ``mido.Message.from_str()``.) Format ^^^^^^ The format is simple:: MESSAGE_TYPE [PARAMETER=VALUE ...] These are the same as the arguments to ``mido.Message()``. The order of parameters doesn't matter but each one can only appear once. Only these characters will ever occur in a string encoded Mido message:: [a-z][0-9][ =_.+()] or written out:: 'abcdefghijklmnopqrstuvwxyz0123456789 =_.+()' This means the message can be embedded in most text formats without any form of escaping. Parsing ^^^^^^^ To *parse* a message, you can use ``mido.parse_string()``:: >>> parse_string('control_change control=1 value=122 time=0.5') Message('control_change', channel=0, control=1, value=122, time=0.5) Parameters that are left out are set to their default values. ``ValueError`` is raised if the message could not be parsed. *Extra whitespace is ignored*:: >>> parse_string(' control_change control=1 value=122') Message('control_change', channel=0, control=1, value=122, time=0) To parse messages from a stream, you can use ``mido.messages.parse_string_stream()``:: for (message, error) in parse_string_stream(open('some_music.text')): if error: print(error) else: do_something_with(message) This will return every valid message in the stream. If a message could not be parsed, ``message`` will be ``None`` and ``error`` will be an error message describing what went wrong, as well as the line number where the error occurred. The argument to ``parse_string_stream()`` can be any object that generates strings when iterated over, such as a file or a list. ``parse_string_stream()`` will ignore blank lines and comments (which start with a ``#`` and go to the end of the line). An example of valid input:: # A very short song with an embedded sysex message. note_on channel=9 note=60 velocity=120 time=0 # Send some data sysex data=(1,2,3) time=0.5 pitchwheel pitch=4000 # bend the not a little time=0.7 note_off channel=9 note=60 velocity=60 time=1.0 Examples ^^^^^^^^ An example of messages embedded into JSON: .. code-block:: json { "messages": [ "0.0 note_on channel=9 note=60 velocity=120", "0.5 sysex data=(1,2,3)", "...", ] } mido-1.3.3/docs/meta_message_types.rst0000644000175100001770000002213514706731605020627 0ustar runnerdocker00000000000000.. SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen .. .. SPDX-License-Identifier: CC-BY-4.0 Meta Message Types ================== Supported Messages ------------------ sequence_number (0x00) ^^^^^^^^^^^^^^^^^^^^^^ =============== ============ ======== Attribute Values Default =============== ============ ======== number 0..65535 0 =============== ============ ======== Sequence number in type 0 and 1 MIDI files; pattern number in type 2 MIDI files. text (0x01) ^^^^^^^^^^^ ============== ============== ======== Attribute Values Default ============== ============== ======== text string '' ============== ============== ======== General "Text" Meta Message. Can be used for any text based data. copyright (0x02) ^^^^^^^^^^^^^^^^ ============== ============== ======== Attribute Values Default ============== ============== ======== text string '' ============== ============== ======== Provides information about a MIDI file's copyright. track_name (0x03) ^^^^^^^^^^^^^^^^^ ============== ============== ======== Attribute Values Default ============== ============== ======== name string '' ============== ============== ======== Stores a MIDI track's name. instrument_name (0x04) ^^^^^^^^^^^^^^^^^^^^^^ ============== ============== ======== Attribute Values Default ============== ============== ======== name string '' ============== ============== ======== Stores an instrument's name. lyrics (0x05) ^^^^^^^^^^^^^ ============== ============== ======== Attribute Values Default ============== ============== ======== text string '' ============== ============== ======== Stores the lyrics of a song. Typically one syllable per Meta Message. marker (0x06) ^^^^^^^^^^^^^ ============== ============== ======== Attribute Values Default ============== ============== ======== text string '' ============== ============== ======== Marks a point of interest in a MIDI file. Can be used as the marker for the beginning of a verse, solo, etc. cue_marker (0x07) ^^^^^^^^^^^^^^^^^ ============== ============== ======== Attribute Values Default ============== ============== ======== text string '' ============== ============== ======== Marks a cue. IE: 'Cue performer 1', etc device_name (0x09) ^^^^^^^^^^^^^^^^^^ ============== ============== ======== Attribute Values Default ============== ============== ======== name string '' ============== ============== ======== Gives the name of the device. channel_prefix (0x20) ^^^^^^^^^^^^^^^^^^^^^ ============== ============== ======== Attribute Values Default ============== ============== ======== channel 0..255 0 ============== ============== ======== Gives the prefix for the channel on which events are played. midi_port (0x21) ^^^^^^^^^^^^^^^^ ============== ============== ======== Attribute Values Default ============== ============== ======== port 0..255 0 ============== ============== ======== Gives the MIDI Port on which events are played. end_of_track (0x2f) ^^^^^^^^^^^^^^^^^^^ ============== ============== ======== Attribute Values Default ============== ============== ======== n/a n/a n/a ============== ============== ======== An empty Meta Message that marks the end of a track. set_tempo (0x51) ^^^^^^^^^^^^^^^^ ============== ============== ======== Attribute Values Default ============== ============== ======== tempo 0..16777215 500000 ============== ============== ======== Tempo is in microseconds per beat (quarter note). You can use :py:func:`bpm2tempo` and :py:func:`tempo2bpm` to convert to and from beats per minute. Note that :py:func:`tempo2bpm` may return a floating point number. smpte_offset (0x54) ^^^^^^^^^^^^^^^^^^^ ============== ================= ======== Attribute Values Default ============== ================= ======== frame_rate 24, 25, 29.97, 30 24 hours 0..255 0 minutes 0..59 0 seconds 0..59 0 frames 0..255 0 sub_frames 0..99 0 ============== ================= ======== time_signature (0x58) ^^^^^^^^^^^^^^^^^^^^^ ============================ =============== ======== Attribute Values Default ============================ =============== ======== numerator 0..255 4 denominator 1..2**255 4 clocks_per_click 0..255 24 notated_32nd_notes_per_beat 0..255 8 ============================ =============== ======== Time signature of: 4/4 : MetaMessage('time_signature', numerator=4, denominator=4) 3/8 : MetaMessage('time_signature', numerator=3, denominator=8) .. versionadded:: 1.2.9 Time signature message have the correct default value of 4/4. In earlier versions the default value was 2/4 due to a typo in the code. key_signature (0x59) ^^^^^^^^^^^^^^^^^^^^ ========= ================== ======== Attribute Values Default ========= ================== ======== key 'C', 'F#m', ... 'C' ========= ================== ======== Valid values: A A#m Ab Abm Am B Bb Bbm Bm C C# C#m Cb Cm D D#m Db Dm E Eb Ebm Em F F# F#m Fm G G#m Gb Gm .. versionchanged:: 1.1.5 The mode attribute was removed. Instead, an 'm' is appended to minor keys. sequencer_specific (0x7f) ^^^^^^^^^^^^^^^^^^^^^^^^^ ============== ============== ======== Attribute Values Default ============== ============== ======== data [..] [] ============== ============== ======== An unprocessed sequencer specific message containing raw data. Unknown Meta Messages --------------------- Unknown meta messages will be returned as ``UnknownMetaMessage`` objects, with ``type`` set to ``unknown_meta``. The messages are saved back to the file exactly as they came out. Code that depends on ``UnknownMetaMessage`` may break if the message in question is ever implemented, so it's best to only use these to learn about the format of the new message and then implement it as described below. ``UnknownMetaMessage`` have two attributes: * ``type_byte`` - a byte which uniquely identifies this message type * ``data`` - the message data as a list of bytes These are also visible in the ``repr()`` string:: UnknownMetaMessage(type_byte=251, data=(1, 2, 3), time=0) Implementing New or Custom Meta Messages ---------------------------------------- If you come across a meta message which is not implemented or you want to use a custom meta message, you can add it by writing a new meta message spec:: from mido.midifiles.meta import MetaSpec, add_meta_spec class MetaSpec_light_color(MetaSpec): type_byte = 0xf0 attributes = ['r', 'g', 'b'] defaults = [0, 0, 0] def decode(self, message, data): # Interpret the data bytes and assign them to attributes. (message.r, message.g, message.b) = data def encode(self, message): # Encode attributes to data bytes and # return them as a list of ints. return [message.r, message.g, message.b] def check(self, name, value): # (Optional) # This is called when the user assigns # to an attribute. You can use this for # type and value checking. (Name checking # is already done. # # If this method is left out, no type and # value checking will be done. if not isinstance(value, int): raise TypeError('{} must be an integer'.format(name)) if not 0 <= value <= 255: raise TypeError('{} must be in range 0..255'.format(name)) Then you can add your new message type with:: add_meta_spec(MetaSpec_light_color) and create messages in the usual way:: >>> from mido import MetaMessage >>> MetaMessage('light_color', r=120, g=60, b=10) MetaMessage('light_color', r=120, g=60, b=10, time=0) and the new message type will now work when reading and writing MIDI files. Some additional functions are available:: encode_string(unicode_string) decode_string(byte_list) These convert between a Unicode string and a list of bytes using the current character set in the file. If your message contains only one string with the attribute name ``text`` or ``name``, you can subclass from one of the existing messages with these attributes, for example:: class MetaSpec_copyright(MetaSpec_text): type_byte = 0x02 class MetaSpec_instrument_name(MetaSpec_track_name): type_byte = 0x04 This allows you to skip everything but ``type_byte``, since the rest is inherited. See the existing ``MetaSpec`` classes for further examples. mido-1.3.3/docs/ports/0000755000175100001770000000000014706731615015364 5ustar runnerdocker00000000000000mido-1.3.3/docs/ports/custom.rst0000644000175100001770000001510014706731605017424 0ustar runnerdocker00000000000000.. SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen .. .. SPDX-License-Identifier: CC-BY-4.0 Writing a New or Custom Port ---------------------------- The Mido port API allows you to write new ports to do practically anything. A new port type can be defined by subclassing one of the base classes and overriding one or more methods. Here's an example:: from mido.ports import BaseOutput class PrintPort(BaseOutput): def _send(message): print(message) >>> port = PrintPort() >>> port.send(msg) note_on channel=0 note=0 velocity=64 time=0 ``_send()`` will be called by ``send()``, and is responsible for actually sending the message somewhere (or in this case print it out). Overridable Methods ^^^^^^^^^^^^^^^^^^^ There are four overridable methods (all of them default to doing nothing):: ``_open(self, **kwargs)`` Should do whatever is necessary to initialize the port (for example opening a MIDI device.) Called by ``__init__()``. The ``name`` attribute is already set when ``_open()`` is called, but you will get the rest of the keyword arguments. If your port takes a different set of arguments or has other special needs, you can override ``__init__()`` instead. ``_close(self)`` Should clean up whatever resources the port has allocated (such as closing a MIDI device). Called by ``close()`` if the port is not already closed. ``_send(self, message)`` (Output ports only.) Should send the message (or do whatever else that makes sense). Called by ``send()`` if the port is open and the message is a Mido message. (You don't need any type checking here.) Raise IOError if something goes wrong. ``_receive(self, block=True)`` (Input ports only.) Should return a message if there is one available. If ``block=True`` it should block until a message is available and then return it. If ``block=False`` it should return a message or ``None`` if there is no message yet. If you return ``None`` the enclosing ``pending()`` method will check ``self._messages`` and return one from there. .. note:: ``Prior to 1.2.0 ``_receive()`` would put messages in ``self._messages`` (usually via the parser) and rely on ``receive()`` to return them to the user. Since this was not thread safe the API was changed in 1.2.0 to allow the ``_receive()`` to return a message. The old behavior is still supported, so old code will work as before. Raise IOError if something goes wrong. Each method corresponds to the public method of the same name, and will be called by that method. The outer method will take care of many things, so the inner method only needs to do the very minimum. The outer method also provides the doc string, so you don't have to worry about that. The base classes are ``BaseInput``, ``BaseOutput`` and ``BaseIOPort`` (which is a subclass of the other two.) Locking ^^^^^^^ The calls to ``_receive()`` and ``_send()`` will are protected by a lock, ``left.lock``. As a result all send and receive will be thread safe. .. note:: If your ``_receive()`` function actually blocks instead of letting the parent class handle it ``poll()`` will not work. The two functions are protected by the same lock, so when ``receive()`` blocks it will also block other threads calling ``poll()``. In this case you need to implement your own locking. If you want to implement your own thread safety you can set the ``_locking`` attribute in your class:: class MyInput(ports.BaseInput): _locking = False ... An example of this is ``mido.backends.rtmidi`` where the callback is used to feed an internal queue that ``receive()`` reads from. Examples ^^^^^^^^ An full example of a device port for the imaginary MIDI library ``fjopp``:: import fjopp from mido.ports import BaseIOPort # This defines an I/O port. class FjoppPort(BaseIOPort): def _open(self, **kwargs): self._device = fjopp.open_device(self.name) def _close(self): self._device.close() def _send(self, message): self.device.write(message.bytes()) def _receive(self, block=True): while True: data = self.device.read() if data: self._parser.feed(data) else: return If ``fjopp`` supports blocking read, you can do this to actually block on the device instead of letting ``receive()`` and friends poll and wait for you:: def _receive(self, block=True): if block: # Actually block on the device. # (``read_blocking()`` will always return some data.) while not ``self._messages``: data = self._device.read_blocking() self._parser.feed(data) else: # Non-blocking read like above. while True: data = self.device.read() if data: self._parser.feed(data) This can be used for any kind of port that wants to block on a pipe, an socket or another input source. Note that Mido will still use polling and waiting when receiving from multiple ports (for example in a ``MultiPort``). If you want separate input and output classes, but the ``_open()`` and ``_close()`` methods have a lot in common, you can implement this using a mix-in. Sometimes it's useful to know inside the methods whether the port supports input or output. The way to do this is to check for the methods ```send()`` and ``receive()``, for example:: def _open(self, **kwargs): if hasattr(self, 'send'): # This is an output port. if hasattr(self, 'receive'): # This is an input port. if hasattr(self, 'send') and hasattr(self, 'receive'): # This is an I/O port. Attributes ^^^^^^^^^^ A port has some attributes that can be useful inside your methods. ``name`` The name of the port. The value is device specific and does not have to be unique. It can have any value, but must be a string or ``None``. This is set by ``__init__()``. ``closed`` True if the port is closed. You don't have to worry about this inside your methods. ``_messages`` This is a ``collections.deque`` of messages that have been read and are ready to be received. This is a shortcut to ``_parser.messages``. ``_device_type`` (Optional.) If this attribute exists, it's a string which will be used in ``__repr__()``. If it doesn't exist, the class name will be used instead. mido-1.3.3/docs/ports/index.rst0000644000175100001770000001704614706731605017234 0ustar runnerdocker00000000000000.. SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen .. .. SPDX-License-Identifier: CC-BY-4.0 Ports ===== A Mido :term:`port` is an *object* that can *send* and/or *receive* messages. You can open a :term:`port` by calling one of the *open methods*, for example:: >>> inport = mido.open_input('SH-201') >>> outport = mido.open_output('Integra-7') Now you can *receive* messages on the *input port* and *send* messages on the *output port*:: >>> msg = inport.receive() >>> outport.send(msg) The message is copied by ``send()``, so you can safely modify your original message without causing breakage in other parts of the system. In this case, the ports are device ports, and are connected to some sort of (physical or virtual) MIDI device, but a port can be anything. For example, you can use a ``MultiPort`` to receive messages from multiple ports as if they were one:: from mido.ports import MultiPort ... multi = MultiPort([inport1, inport2, inport3]) for msg in multi: print(msg) This will receive messages from all ports and print them out. Another example is a socket port, which is a wrapper around a TCP/IP socket. No matter how the port is implemented internally or what it does, it will look and behave like any other Mido port, so all kinds of ports can be used interchangeably. .. warning:: Sending and receiving messages is thread safe. Opening and closing ports and listing port names are not. Common ------ How to open a :term:`port` depends on the port type. Device ports (PortMidi, RtMidi and others defined in backends) are opened with the open functions, for example:: port = mido.open_output() Input and I/O ports (which support both input and output) are opened with ``open_input()`` and ``open_ioport()`` respectively. If you call these without a port name like above, you will get the - system specific - default port. You can override this by setting the ``MIDO_DEFAULT_OUTPUT`` etc. environment variables. To get a *list* of available ports, you can do:: >>> mido.get_output_names() ['SH-201', 'Integra-7'] and then:: >>> port = mido.open_output('Integra-7') There are corresponding functions for input and I/O ports. To learn how to open other kinds of ports, see documentation of the relevant port type. The *port name* is available in ``port.name``. To *close* a port, call:: port.close() or use the ``with`` statement to have the port closed automatically:: with mido.open_input() as port: for message in port: do_something_with(message) You can check if the *port is closed* with:: if port.closed: print("Yup, it's closed.") If the port is already closed, calling ``close()`` will simply do nothing. Output ------ Output :term:`ports` basically only have one method:: outport.send(message) This will *send* the message immediately. (Well, the port can choose to do whatever it wants with the message, but at least it's sent from Mido's point of view.) There are also a couple of utility methods:: outport.reset() This will send "all notes off" and "reset all controllers" on every channel. This is used to reset everything to the default state, for example after playing back a song or messing around with controllers. If you pass ``autoreset=True`` to the constructor, ``reset()`` will be called when the port closes:: with mido.open_output('Integra-7') as outport: for msg in inport: outport.send(msg) # reset() is called here outport.close() # or here Sometimes notes hang because a ``note_off`` has not been sent. To (abruptly) stop all sounding notes, you can call:: outport.panic() This will not reset controllers. Unlike ``reset()``, the notes will not be turned off gracefully, but will stop immediately with no regard to decay time. Input ----- To *iterate* over *incoming messages*:: for msg in port: print(msg) This will iterate over messages as they arrive on the port until the port closes. (So far only socket ports actually close by themselves. This happens if the other end disconnects.) You can also do *non-blocking iteration*:: for msg in port.iter_pending(): print(msg) This will iterate over all messages that have already arrived. It is typically used in main loops where you want to do something else while you wait for messages:: while True: for msg in port.iter_pending(): print(msg) do_other_stuff() In an *event based system* like a GUI where you don't write the main loop you can install a *handler* that's called periodically. Here's an example for GTK:: def callback(self): for msg in self.inport: print(msg) gobject.timeout_add_seconds(timeout, callback) To get a bit more control you can receive messages *one at a time*:: msg = port.receive() This will *block* until a message arrives. To get a message only if one is available, you can use `poll()`:: msg = port.poll() This will return ``None`` immediately if *no message is available*. .. deprecated:: 1.2 There used to be a ``pending()`` method which returned the number of pending messages. It was removed for three reasons: * with ``poll()`` and ``iter_pending()`` it is no longer necessary * it was unreliable when multithreading and for some ports it doesn't even make sense * it made the internal method API confusing. `_send()` sends a message so `_receive()` should receive a message. Callbacks --------- Instead of manually reading from the :term:`port` you can install a :term:`callback` function which will be called for every message that arrives. Here's a simple callback function:: def print_message(message): print(message) To *install* the callback you can either pass it when you create the port or later by setting the ``callback`` attribute:: port = mido.open_input(callback=print_message) port.callback = print_message ... port.callback = another_function .. warning:: Since the :term:`callback` runs in a different thread you may need to use locks or other synchronization mechanisms to keep your main program and the callback from stepping on each other's toes. Calling ``receive()``, ``__iter__()``, or ``iter_pending()`` on a :term:`port` with a :term:`callback` will raise an exception:: ValueError: a callback is set for this port To *clear* the :term:`callback`:: port.callback = None This will return the :term:`port` to normal. API --- .. todo:: Add abstract code to describe these interfaces. Common Methods and Attributes ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ``close()`` Closes the :term:`port`. If the :term:`port` is already closed this will simply do nothing. ``name`` Name of the port or ``None``. ``closed`` ``True`` if the port is closed. Output Port Methods ^^^^^^^^^^^^^^^^^^^ ``send(message)`` Sends a message. ``reset()`` Sends "all notes off" and "reset all controllers" on all channels. ``panic()`` Sends "all sounds off" on all channels. This will abruptly end all sounding notes. Input Port Methods ^^^^^^^^^^^^^^^^^^ ``receive(block=True)`` Receives a message. This will block until it returns a message. If ``block=False`` is passed it will instead return ``None`` if there is no message. ``poll()`` Returns a message, or ``None`` if there are no pending messages. ``iter_pending()`` Iterates through pending messages. ``__iter__()`` Iterates through messages as they arrive on the :term:`port` until the :term:`port` closes. .. include:: socket.rst .. include:: custom.rst mido-1.3.3/docs/ports/socket.rst0000644000175100001770000001033614706731605017410 0ustar runnerdocker00000000000000.. SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen .. .. SPDX-License-Identifier: CC-BY-4.0 Socket Ports - MIDI over TCP/IP ------------------------------- About ^^^^^ Socket :term:`ports` allows you to send :term:`MIDI` messages over a computer network. The protocol is a simple MIDI bytes stream over :term:`TCP`. .. warning:: It is **not** :term:`rtpmidi`! Caveats ^^^^^^^ The data is sent over an *unencrypted channel*. Also, the default server allows connections from any host and also accepts arbitrary :term:`sysex` messages, which could allow anyone to for example overwrite patches on your synths (or **worse**). Use **only** on *trusted networks*. If you need more security, you can build a *custom server* with a whitelist of clients allowed to connect. If *timing* is critical, *latency* and *jitter* (especially on *wireless networks*) may make socket ports *unusable*. Sending Messages to a Server ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ First, let's import some things:: from mido.sockets import PortServer, connect After that, a simple server is only two lines:: for message in PortServer('localhost', 8080): print(message) You can then connect to the server and send it messages:: output = connect('localhost', 8080): output.send(message) Each end of the connection behaves like a normal Mido I/O port, with all the usual methods. The host may be an host name or IP address (as a string). It may also be '', in which case connections are accepted from any IP address on the computer. .. todo:: Test and clarify "Any IP address on the computer". Does this mean only local adresses can connect or that any connection from any network is allowed? Turning Things on their Head ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ If you want the server to send messages the client, you can instead do:: server = PortServer('localhost', 8080): while True: server.send(message) ... and then on the client side:: for message in connect('localhost', 8080): print(message) The client will now print any message that the server sends. Each message that the server sends will be received by all connected clients. Under the Hood ^^^^^^^^^^^^^^ The examples above use the server and client ports as normal Mido I/O ports. This makes it easy to write simple servers, but you don't have any control on connections and the way messages are sent and received. To get more control,you can ignore all the other methods of the ``PortServer`` object and use only ``accept()``. Here's a simple server implemented this way:: with PortServer('localhost', 8080) as server: while True: client = server.accept() for message in client: print(message) ``accept()`` waits for a client to connect, and returns a ``SocketPort`` object which is connected to the ``SocketPort`` object returned by ``connect()`` on the other end. The server above has one weakness: it only allows one connection at a time. You can get around this by using ``accept(block=False)``. This will return a ``SocketPort`` if there's a connection waiting and ``None`` if there is connection yet. .. todo:: Clarify "Connection waiting" vs "There is a connection yet". Using this you can write the server any way you like, for example:: with PortServer('localhost', 8080) as server: clients = [] while True: # Handle connections. client = server.accept(block=False) if client: print('Connection from {}'.format(client.name)) clients.append(client) for i, client in reversed(enumerate(clients)): if client.closed: print('{} disconnected'.format(client.name)) del clients[i] # Receive messages. for client in clients: for message in client.iter_pending() print('Received {} from {}'.format(message, client)) # Do other things ... Possible Future Additions ^^^^^^^^^^^^^^^^^^^^^^^^^ Optional HTTP-style headers could be added. As long as these are 7-bit :term:`ASCII`, they will be counted as data bytes and ignored by clients or servers who don't expect them. mido-1.3.3/docs/resources.rst0000644000175100001770000000244614706731605016766 0ustar runnerdocker00000000000000.. SPDX-FileCopyrightText: 2014 Ole Martin Bjorndalen .. SPDX-FileCopyrightText: 2023 Raphaël Doursenaud .. .. SPDX-License-Identifier: CC-BY-4.0 Resources ========= * `MIDI Association `_ * `An Introduction to MIDI `_ * `Official MIDI 1.0 detail specification `_ * `Standard MIDI Files Specification `_ * `MIDI Reference Tables `_ * `MIDI `_ (Wikipedia) * `Essentials of the MIDI Protocol `_ (Craig Stuart Sapp, CCRMA) * `Outline of the Standard MIDI File Structure `_ (Craig Stuart Sapp, CCRMA) * `Active Sensing `_ (Sweetwater) * `MIDI File Parsing `_ (Course assignment in `Music 253 `_ at Stanford University) * `Meta Message `_ (Sound On Sound) mido-1.3.3/docs/shared/0000755000175100001770000000000014706731615015463 5ustar runnerdocker00000000000000mido-1.3.3/docs/shared/licenses_logos.rst0000644000175100001770000000112214706731605021220 0ustar runnerdocker00000000000000.. SPDX-FileCopyrightText: 2023 Raphaël Doursenaud .. .. SPDX-License-Identifier: CC-BY-4.0 .. |Creative Commons BY-4.0 License| image:: https://licensebuttons.net/l/by/4.0/88x31.png :target: https://creativecommons.org/licenses/by/4.0/ .. |Creative Commons BY-SA-3.0 License| image:: https://licensebuttons.net/l/by-sa/3.0/88x31.png :target: https://creativecommons.org/licenses/by-sa/3.0/ .. |Creative Commons CC0-1.0 License| image:: https://licensebuttons.net/p/zero/1.0/88x31.png :target: https://creativecommons.org/publicdomain/zero/1.0/ mido-1.3.3/examples/0000755000175100001770000000000014706731615015103 5ustar runnerdocker00000000000000mido-1.3.3/examples/backends/0000755000175100001770000000000014706731615016655 5ustar runnerdocker00000000000000mido-1.3.3/examples/backends/printer.py0000644000175100001770000000124414706731605020712 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT """ A simple custom backend with an output port type which prints messages to stdout. """ from mido.ports import BaseOutput def get_devices(): return [{'name': 'The Print Port', 'is_input': False, 'is_output': True}] class Output(BaseOutput): def _open(self, **kwargs): device = get_devices()[0] if self.name is None: self.name = device['name'] elif self.name != device['name']: raise ValueError(f'unknown port {self.name!r}') def _send(self, message): print(message) mido-1.3.3/examples/backends/rtm.py0000644000175100001770000000610414706731605020031 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT """ Experimental backend for rtmidi-python: http://github.com/superquadratic/rtmidi-python - Doesn't work with Python 3.3: File "rtmidi_python.pyx", line 61, in rtmidi_python.MidiIn.__cinit__ (rtmidi_ python.cpp:1214) TypeError: expected bytes, str found - Virtual ports don't show up, so other programs can't connect to them. - There is no way to select API. Other than that, it works exactly like the included python-rtmidi backend. """ import rtmidi_python as rtmidi from mido.ports import BaseInput, BaseOutput def get_devices(): devices = [] input_names = set(rtmidi.MidiIn().ports) output_names = set(rtmidi.MidiOut().ports) for name in sorted(input_names | output_names): devices.append({ 'name': name, 'is_input': name in input_names, 'is_output': name in output_names, }) return devices class PortCommon: def _open(self, virtual=False, callback=None): opening_input = hasattr(self, 'receive') if opening_input: self._rt = rtmidi.MidiIn() self._rt.ignore_types(False, False, False) if callback: def callback_wrapper(message, delta_time): self._parser.feed(message) for message in self._parser: callback(message) self._rt.callback = self._callback = callback_wrapper self._has_callback = True else: self._has_callback = False else: self._rt = rtmidi.MidiOut() # Turn of ignore of sysex, time and active_sensing. ports = self._rt.ports if virtual: if self.name is None: raise OSError('virtual port must have a name') self._rt.open_virtual_port(self.name) else: if self.name is None: # Todo: this could fail if list is empty. # In RtMidi, the default port is the first port. try: self.name = ports[0] except IndexError as ie: raise OSError('no ports available') from ie try: port_id = ports.index(self.name) except ValueError as ve: raise OSError(f'unknown port {self.name!r}') from ve self._rt.open_port(port_id) self._device_type = 'rtmidi_python' def _close(self): self._rt.close_port() class Input(PortCommon, BaseInput): # Todo: sysex messages do not arrive here. def _receive(self, block=True): if self._has_callback: raise OSError('a callback is currently set for this port') while True: (message, delta_time) = self._rt.get_message() if message is None: break else: self._parser.feed(message) class Output(PortCommon, BaseOutput): def _send(self, message): self._rt.send_message(message.bytes()) mido-1.3.3/examples/backends/use_printer.py0000644000175100001770000000116514706731605021570 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2014 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT """ Try out the new printer port backend. It also works with MIDO_BACKEND, so you can do: $ MIDO_BACKEND=printer python >>> import mido >>> mido.get_output_names() ['The Printer Port'] """ import mido mido.set_backend('printer') print(f'Available outputs: {mido.get_output_names()!r}') with mido.open_output() as port: print(f'Using {port}.') port.send(mido.Message('program_change', program=10)) for i in [1, 2, 3]: port.send(mido.Message('control_change', control=1, value=i)) mido-1.3.3/examples/midifiles/0000755000175100001770000000000014706731615017050 5ustar runnerdocker00000000000000mido-1.3.3/examples/midifiles/create_midi_file.py0000755000175100001770000000166114706731605022674 0ustar runnerdocker00000000000000#!/usr/bin/env python3 # SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT """ Create a new MIDI file with some random notes. The file is saved to test.mid. """ import random import sys from mido import MAX_PITCHWHEEL, Message, MidiFile, MidiTrack notes = [64, 64 + 7, 64 + 12] outfile = MidiFile() track = MidiTrack() outfile.tracks.append(track) track.append(Message('program_change', program=12)) delta = 300 ticks_per_expr = int(sys.argv[1]) if len(sys.argv) > 1 else 20 for i in range(4): note = random.choice(notes) track.append(Message('note_on', note=note, velocity=100, time=delta)) for j in range(delta // ticks_per_expr): pitch = MAX_PITCHWHEEL * j * ticks_per_expr // delta track.append(Message('pitchwheel', pitch=pitch, time=ticks_per_expr)) track.append(Message('note_off', note=note, velocity=100, time=0)) outfile.save('test.mid') mido-1.3.3/examples/midifiles/midifile_to_json.py0000644000175100001770000000071014706731605022734 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2016 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT import json import sys import mido def midifile_to_dict(mid): tracks = [] for track in mid.tracks: tracks.append([vars(msg).copy() for msg in track]) return { 'ticks_per_beat': mid.ticks_per_beat, 'tracks': tracks, } mid = mido.MidiFile(sys.argv[1]) print(json.dumps(midifile_to_dict(mid), indent=2)) mido-1.3.3/examples/midifiles/play_midi_file.py0000755000175100001770000000144614706731605022377 0ustar runnerdocker00000000000000#!/usr/bin/env python3 # SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT """ Play MIDI file on output port. Run with (for example): ./play_midi_file.py 'SH-201 MIDI 1' 'test.mid' """ import sys import time import mido from mido import MidiFile filename = sys.argv[1] if len(sys.argv) == 3: portname = sys.argv[2] else: portname = None with mido.open_output(portname) as output: try: midifile = MidiFile(filename) t0 = time.time() for message in midifile.play(): print(message) output.send(message) print('play time: {:.2f} s (expected {:.2f})'.format( time.time() - t0, midifile.length)) except KeyboardInterrupt: print() output.reset() mido-1.3.3/examples/midifiles/print_midi_file.py0000755000175100001770000000104314706731605022557 0ustar runnerdocker00000000000000#!/usr/bin/env python3 # SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT """ Open a MIDI file and print every message in every track. Support for MIDI files is still experimental. """ import sys from mido import MidiFile if __name__ == '__main__': filename = sys.argv[1] midi_file = MidiFile(filename) for i, track in enumerate(midi_file.tracks): sys.stdout.write(f'=== Track {i}\n') for message in track: sys.stdout.write(f' {message!r}\n') mido-1.3.3/examples/midifiles/show_midifile.py0000755000175100001770000000057314706731605022253 0ustar runnerdocker00000000000000#!/usr/bin/env python3 # SPDX-FileCopyrightText: 2020 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT import argparse import mido def parse_args(): parser = argparse.ArgumentParser() arg = parser.add_argument arg('midifile', nargs=1) return parser.parse_args() args = parse_args() print(repr(mido.MidiFile(args.midifile[0]))) mido-1.3.3/examples/midifiles/test.sh0000755000175100001770000000036314706731605020367 0ustar runnerdocker00000000000000#!/bin/bash # SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT function play { # mido-play test.mid pmidi -p 20:0 -d 0 test.mid } ./create_midi_file.py && xxd test.mid && play mido-1.3.3/examples/ports/0000755000175100001770000000000014706731615016252 5ustar runnerdocker00000000000000mido-1.3.3/examples/ports/input_filter.py0000755000175100001770000000163014706731605021332 0ustar runnerdocker00000000000000#!/usr/bin/env python3 # SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT """ Receive messages on the input port and print messages with a specific channel (defaults to 0). Usage: python3 example_input_filter.py portname [CHANNEL] [...] """ import sys import mido def accept_notes(port): """Only let note_on and note_off messages through.""" for message in port: if message.type in ('note_on', 'note_off'): yield message if len(sys.argv) > 1: portname = sys.argv[1] else: portname = None # Use default port try: with mido.open_input(portname) as port: print(f'Using {port}') print("Ignoring everything but 'note_on' and 'note_off'.") print('Waiting for notes...') for message in accept_notes(port): print(f'Received {message}') except KeyboardInterrupt: pass mido-1.3.3/examples/ports/list_ports.py0000755000175100001770000000071514706731605021033 0ustar runnerdocker00000000000000#!/usr/bin/env python3 # SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT """ List available ports. This is a simple version of mido-ports. """ import mido def print_ports(heading, port_names): print(heading) for name in port_names: print(f" '{name}'") print() print() print_ports('Input Ports:', mido.get_input_names()) print_ports('Output Ports:', mido.get_output_names()) mido-1.3.3/examples/ports/multi_receive.py0000755000175100001770000000101214706731605021454 0ustar runnerdocker00000000000000#!/usr/bin/env python3 # SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT """ Receive messages from multiple ports. """ import mido from mido.ports import multi_receive # Open all available inputs. ports = [mido.open_input(name) for name in mido.get_input_names()] for port in ports: print(f'Using {port}') print('Waiting for messages...') try: for message in multi_receive(ports): print(f'Received {message}') except KeyboardInterrupt: pass mido-1.3.3/examples/ports/nonblocking_receive.py0000755000175100001770000000117614706731605022640 0ustar runnerdocker00000000000000#!/usr/bin/env python3 # SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT """ Example of non-blocking reception from input port. """ import sys import time import mido if len(sys.argv) > 1: portname = sys.argv[1] else: portname = None # Use default port try: with mido.open_input(portname) as port: print(f'Using {port}') while True: for message in port.iter_pending(): print(f'Received {message}') print('Doing something else for a while...') time.sleep(0.5) except KeyboardInterrupt: pass mido-1.3.3/examples/ports/queue_port.py0000644000175100001770000000156614706731605021023 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT """A port interface around a queue. This allows you to create internal ports to send messages between threads. """ import queue import mido from mido import ports class QueuePort(ports.BaseIOPort): # We don't need locking since the queue takes care of that. _locking = False def _open(self): self.queue = queue.Queue() def _send(self, msg): self.queue.put(msg) def _receive(self, block=True): try: return self.queue.get(block=block) except queue.Empty: return None def _device_type(self): return 'Queue' def main(): msg = mido.Message('clock') port = QueuePort() print(port.poll()) port.send(msg) print(port.receive()) if __name__ == '__main__': main() mido-1.3.3/examples/ports/receive.py0000755000175100001770000000107714706731605020255 0ustar runnerdocker00000000000000#!/usr/bin/env python3 # SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT """ Receive messages from the input port and print them out. """ import sys import mido if len(sys.argv) > 1: portname = sys.argv[1] else: portname = None # Use default port try: with mido.open_input(portname) as port: print(f'Using {port}') print('Waiting for messages...') for message in port: print(f'Received {message}') sys.stdout.flush() except KeyboardInterrupt: pass mido-1.3.3/examples/ports/send.py0000755000175100001770000000156614706731605017567 0ustar runnerdocker00000000000000#!/usr/bin/env python3 # SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT """ Send random notes to the output port. """ import random import sys import time import mido from mido import Message if len(sys.argv) > 1: portname = sys.argv[1] else: portname = None # Use default port # A pentatonic scale notes = [60, 62, 64, 67, 69, 72] try: with mido.open_output(portname, autoreset=True) as port: print(f'Using {port}') while True: note = random.choice(notes) on = Message('note_on', note=note) print(f'Sending {on}') port.send(on) time.sleep(0.05) off = Message('note_off', note=note) print(f'Sending {off}') port.send(off) time.sleep(0.1) except KeyboardInterrupt: pass print() mido-1.3.3/examples/sockets/0000755000175100001770000000000014706731615016556 5ustar runnerdocker00000000000000mido-1.3.3/examples/sockets/forward_ports.py0000755000175100001770000000113414706731605022024 0ustar runnerdocker00000000000000#!/usr/bin/env python3 # SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT """ Forward all messages from one or more ports to server. Example: python3 forward_ports.py localhost:8080 'Keyboard MIDI 1' """ import sys import mido host, port = mido.sockets.parse_address(sys.argv[1]) ports = [mido.open_input(name) for name in sys.argv[2:]] with mido.sockets.connect(host, port) as server_port: print('Connected.') for message in mido.ports.multi_receive(ports): print(f'Sending {message}') server_port.send(message) mido-1.3.3/examples/sockets/serve_ports.py0000755000175100001770000000163614706731605021513 0ustar runnerdocker00000000000000#!/usr/bin/env python3 # SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT """ Serve one or more output ports. Every message received on any of the connected sockets will be sent to every output port. Example: python3 serve_ports.py :8080 'SH-201' 'SD-20 Part A' This simply iterates through all incoming messages. More advanced and flexible servers can be written by calling the ``accept()`` and ``accept(block=False) methods directly. See PortServer.__init__() for an example. """ import sys import mido from mido import sockets from mido.ports import MultiPort # Todo: do this with a argument parser. out = MultiPort([mido.open_output(name) for name in sys.argv[2:]]) (host, port) = sockets.parse_address(sys.argv[1]) with sockets.PortServer(host, port) as server: for message in server: print(f'Received {message}') out.send(message) mido-1.3.3/examples/sockets/simple_client.py0000755000175100001770000000175414706731605021770 0ustar runnerdocker00000000000000#!/usr/bin/env python3 # SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT """ Simple client that sends program_change messages to server at timed intervals. Example: python3 simple_client.py localhost:8080 """ import random import sys import time import mido if sys.argv[1:]: address = sys.argv[1] else: address = 'localhost:9080' host, port = mido.sockets.parse_address(address) notes = [60, 67, 72, 79, 84, 79, 72, 67, 60] on = mido.Message('note_on', velocity=100) off = mido.Message('note_off', velocity=100) base = random.randrange(12) print(f'Connecting to {address}') with mido.sockets.connect(host, port) as server_port: try: message = mido.Message('program_change') for note in notes: on.note = off.note = base + note server_port.send(on) time.sleep(0.05) server_port.send(off) time.sleep(0.1) finally: server_port.reset() mido-1.3.3/examples/sockets/simple_server.py0000755000175100001770000000114014706731605022005 0ustar runnerdocker00000000000000#!/usr/bin/env python3 # SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT """ Simple server that just prints every message it receives. python3 simple_server.py localhost:8080 """ import sys from mido import sockets if sys.argv[1:]: address = sys.argv[1] else: address = 'localhost:9080' try: (hostname, portno) = sockets.parse_address(address) print(f'Serving on {address}') with sockets.PortServer(hostname, portno) as server: for message in server: print(message) except KeyboardInterrupt: pass mido-1.3.3/examples/using_rtmidi_directly.py0000644000175100001770000000142014706731605022045 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT """ First example from here modified to use Mido messages: http://pypi.python.org/pypi/python-rtmidi/ """ import time import rtmidi import mido midiout = rtmidi.MidiOut() available_ports = midiout.get_ports() if available_ports: midiout.open_port(0) else: midiout.open_virtual_port("My virtual output") # Original example: # note_on = [0x99, 60, 112] # channel 10, middle C, velocity 112 # note_off = [0x89, 60, 0] note_on = mido.Message('note_on', channel=9, note=60, velocity=112).bytes() note_off = mido.Message('note_off', channel=9, note=60, velocity=0).bytes() midiout.send_message(note_on) time.sleep(0.5) midiout.send_message(note_off) del midiout mido-1.3.3/extras/0000755000175100001770000000000014706731615014573 5ustar runnerdocker00000000000000mido-1.3.3/extras/README.rst0000644000175100001770000000061414706731605016262 0ustar runnerdocker00000000000000.. SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen .. .. SPDX-License-Identifier: CC-BY-4.0 Disclaimer =========== These are experimental versions of functionality that may be rolled into Mido some time in the future. Until the, proceed at your own risk. (Where risk means the danger the code will crash or that the API will change once you had just gotten used to it.) mido-1.3.3/extras/hid_joystick.py0000644000175100001770000001535314706731605017636 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT """Read from /dev/input/js0 and return as dictionaries. If you have pygame it is easier and more portable to do something like:: import pygame.joystick from pygame.event import event_name pygame.init() pygame.joystick.init() js = pygame.joystick.Joystick(0) js.init() while True: for event in pygame.event.get(): if event.axis == 0: print(event) Init: 8 = init? Time stamp | (ms since boot) | --+--+--+-- | -- Button number f0 fb 37 09 00 00 81 00 f0 fb 37 09 00 00 81 01 f0 fb 37 09 00 00 81 02 f0 fb 37 09 00 00 81 03 f0 fb 37 09 00 00 81 04 f0 fb 37 09 00 00 81 05 f0 fb 37 09 00 00 81 06 f0 fb 37 09 00 00 81 07 f0 fb 37 09 00 00 81 08 f0 fb 37 09 00 00 81 09 f0 fb 37 09 00 00 81 0a f0 fb 37 09 00 00 81 0b f0 fb 37 09 00 00 82 00 f0 fb 37 09 00 00 82 01 f0 fb 37 09 00 00 82 02 f0 fb 37 09 00 00 82 03 f0 fb 37 09 00 00 82 04 f0 fb 37 09 00 00 82 05 --+-- | | 1 = button, 2 = | value (little endian unsigned) button down | 98 f0 2f 09 01 00 01 00 1 down 08 fa 2f 09 00 00 01 00 1 up 2c 6a 31 09 01 00 01 01 2 down 04 73 31 09 00 00 01 01 2 up 48 bf 32 09 01 00 01 02 3 down f8 c4 32 09 00 00 01 02 3 up Logitech PS2-style gamepad: axis 0 == left stick -left / right (left is negative) axis 1 == left stick -up / down (up is negative) axis 2 == right stick -left / right axis 3 == right stick -up / down axis 4 == plus stick -left / right (when mode is off), values min/0/max axis 5 == plus stick -up / down (when mode is off, values min/0/max The + stick has two modes. When the mode light is off, it sends axis 4/5. When mode is on, it sends axis 0/1. The values are -32767, 0, and 32767. Other axis have values from -32767 to 32767 as well. """ import struct JS_EVENT_BUTTON = 0x1 JS_EVENT_AXIS = 0x2 JS_EVENT_INIT = 0x80 def read_event(device): data = device.read(8) event = {} (event['time'], event['value'], event['type'], event['number']) = struct.unpack('IhBB', data) event['init'] = bool(event['type'] & JS_EVENT_INIT) event['type'] &= 0x7f # Strip away the flag bits (JS_EVENT_INIT etc.) if event['type'] != JS_EVENT_BUTTON: event['normalized_value'] = \ float(event['value']) / 0x7fff # Normalize to -1..1 event['type'] = {1: 'button', 2: 'axis'}[event['type']] return event def read_events(device_name): with open(device_name, 'rb') as device: while True: yield read_event(device) def panic(port): """ Send "All Notes Off" and "Reset All Controllers" on all channels. """ for channel in range(16): for control in [121, 123]: message = mido.Message('control_change', channel=channel, control=control, value=0) print(message) port.send(message) class Monophonic: # Todo: this assumes everything is on channel 0! def __init__(self, output, channel=0): self.output = output self.notes = set() self.current_note = None self.channel = channel def send(self, message): if message.type not in ['note_on', 'note_off'] or \ message.channel != self.channel: self.output.send(message) return if message.type == 'note_on': self.notes.add(message.note) elif message.type == 'note_off': if message.note in self.notes: self.notes.remove(message.note) print(self.notes) try: note = min(self.notes) except ValueError: note = None if note == self.current_note: return # Same note as before, no change. if self.current_note is not None: off = mido.Message('note_off', note=self.current_note, velocity=message.velocity) print(off) self.output.send(off) self.current_note = None if note is not None: on = mido.Message('note_on', note=note, velocity=message.velocity) print(on) self.output.send(on) self.current_note = note def play_scale(dev, out): # out = Monophonic(out, channel=0) # Major scale. scale = [0, 2, 4, 5, 7, 9, 11, 12, 14, 16, 17, 19] # program = 16 # Organ program = 74 out.send(mido.Message('program_change', program=program)) while True: event = read_event(dev) if event['init']: continue # Skip init events. if event['type'] == 'button': # Convert to D-major scale starting at middle D. note = 62 + 12 + scale[event['number']] if event['value']: type_ = 'note_on' else: type_ = 'note_off' message = mido.Message(type_, note=note, velocity=64) out.send(message) # elif event['type'] == 'axis': # if event['number'] == 0: # pitch_scale = mido.messages.MAX_PITCHWHEEL # pitch = int(event['normalized_value'] * pitch_scale) # out.send(mido.Message('pitchwheel', pitch=pitch)) def play_drums(dev, out): # http://www.midi.org/techspecs/gm1sound.php note_mapping = { 2: 35, # Acoustic Bass Drum 6: 38, # Acoustic Snare 1: 41, # Low Floor Tom 4: 47, # Low Mid Tom 3: 50, # High Tom 8: 51, # Ride Cymbal 5: 42, # Closed Hi Hat 7: 46, # Open Hi Hat 9: 52, # Chinese Cymbal 10: 55, # Splash Cymbal } while True: event = read_event(dev) if event['init']: continue if event['type'] == 'button': print(event) button = event['number'] + 1 # Number buttons starting with 1. if button not in note_mapping: continue if event['value']: type_ = 'note_on' else: type_ = 'note_off' note = note_mapping[button] message = mido.Message(type_, channel=9, note=note, velocity=64) print(message) out.send(message) if __name__ == '__main__': import mido with open('/dev/input/js0') as dev: with mido.open_output('SD-20 Part A') as out: try: # play_drums(dev, out) play_scale(dev, out) finally: panic(out) mido-1.3.3/mido/0000755000175100001770000000000014706731615014215 5ustar runnerdocker00000000000000mido-1.3.3/mido/__init__.py0000644000175100001770000001104314706731605016324 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT """ MIDI Objects for Python Mido is a library for working with MIDI messages and ports. It's designed to be as straight forward and Pythonic as possible. Creating messages: Message(type, **parameters) -- create a new message MetaMessage(type, **parameters) -- create a new meta message UnknownMetaMessage(type_byte, data=None, time=0) Ports: open_input(name=None, virtual=False, callback=None) -- open an input port open_output(name=None, virtual=False, -- open an output port autoreset=False) open_ioport(name=None, virtual=False, -- open an I/O port (capable callback=None, autoreset=False) of both input and output) get_input_names() -- return a list of names of available input ports get_output_names() -- return a list of names of available output ports get_ioport_names() -- return a list of names of available I/O ports MIDI files: MidiFile(filename, **kwargs) -- open a MIDI file MidiTrack() -- a MIDI track bpm2tempo() -- convert beats per minute to MIDI file tempo tempo2bpm() -- convert MIDI file tempo to beats per minute merge_tracks(tracks) -- merge tracks into one track SYX files: read_syx_file(filename) -- read a SYX file write_syx_file(filename, messages, plaintext=False) -- write a SYX file Parsing MIDI streams: parse(bytes) -- parse a single message bytes (any iterable that generates integers in 0..127) parse_all(bytes) -- parse all messages bytes Parser -- MIDI parser class Parsing objects serialized with str(message): parse_string(string) -- parse a string containing a message parse_string_stream(iterable) -- parse strings from an iterable and generate messages Sub modules: ports -- useful tools for working with ports For more on MIDI, see: http://www.midi.org/ Getting started: >>> import mido >>> m = mido.Message('note_on', note=60, velocity=64) >>> m >>> m.type 'note_on' >>> m.channel = 6 >>> m.note = 19 >>> m.copy(velocity=120) >>> s = mido.Message('sysex', data=[byte for byte in range(5)]) >>> s.data (0, 1, 2, 3, 4) >>> s.hex() 'F0 00 01 02 03 04 F7' >>> len(s) 7 >>> default_input = mido.open_input() >>> default_input.name 'MPK mini MIDI 1' >>> output = mido.open_output('SD-20 Part A') >>> >>> for message in default_input: ... output.send(message) >>> get_input_names() ['MPK mini MIDI 1', 'SH-201'] """ from . import ports, sockets from .backends.backend import Backend from .messages import ( MAX_PITCHWHEEL, MAX_SONGPOS, MIN_PITCHWHEEL, MIN_SONGPOS, Message, format_as_string, parse_string, parse_string_stream, ) from .midifiles import ( KeySignatureError, MetaMessage, MidiFile, MidiTrack, UnknownMetaMessage, bpm2tempo, merge_tracks, second2tick, tempo2bpm, tick2second, ) from .parser import Parser, parse, parse_all from .syx import read_syx_file, write_syx_file from .version import version_info __all__ = [ "KeySignatureError", "MAX_PITCHWHEEL", "MAX_SONGPOS", "MIN_PITCHWHEEL", "MIN_SONGPOS", "Message", "MetaMessage", "MidiFile", "MidiTrack", "Parser", "UnknownMetaMessage", "bpm2tempo", "format_as_string", "merge_tracks", "parse", "parse_all", "parse_string", "parse_string_stream", "ports", "read_syx_file", "second2tick", "sockets", "tempo2bpm", "tick2second", "version_info", "write_syx_file", ] def set_backend(name=None, load=False): """Set current backend. name can be a module name like 'mido.backends.rtmidi' or a Backend object. If no name is passed, the default backend will be used. This will replace all the open_*() and get_*_name() functions in top level mido module. The module will be loaded the first time one of those functions is called.""" glob = globals() if isinstance(name, Backend): backend = name else: backend = Backend(name, load=load, use_environ=True) glob['backend'] = backend for name in dir(backend): if name.split('_')[0] in ['open', 'get']: glob[name] = getattr(backend, name) set_backend() mido-1.3.3/mido/backends/0000755000175100001770000000000014706731615015767 5ustar runnerdocker00000000000000mido-1.3.3/mido/backends/__init__.py0000644000175100001770000000015314706731605020076 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT mido-1.3.3/mido/backends/_parser_queue.py0000644000175100001770000000253514706731605021204 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2017 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT import queue from threading import RLock from ..parser import Parser class ParserQueue: """ Thread safe message queue with built in MIDI parser. This should be avaiable to other backend implementations and perhaps also in the public API, but the API needs a bit of review. (Ideally This would replace the parser.) q = ParserQueue() q.put(msg) q.put_bytes([0xf8, 0, 0]) msg = q.get() msg = q.poll() """ def __init__(self): self._queue = queue.Queue() self._parser = Parser() self._parser_lock = RLock() def put(self, msg): self._queue.put(msg) def put_bytes(self, msg_bytes): with self._parser_lock: self._parser.feed(msg_bytes) for msg in self._parser: self.put(msg) # TODO: add timeout? def get(self): return self._queue.get() def poll(self): try: return self._queue.get_nowait() except queue.Empty: return None def __iter__(self): while True: return self.get() def iterpoll(self): while True: msg = self.poll() if msg is None: return else: yield msg mido-1.3.3/mido/backends/amidi.py0000644000175100001770000000624314706731605017430 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2016 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT """Mido amidi backend Very experimental backend using amidi to access the ALSA rawmidi interface. TODO: * use parser instead of from_hex()? * default port name * do sysex messages work? * starting amidi for every message sent is costly """ import select import subprocess import threading from ..messages import Message from ._common import InputMethods, OutputMethods, PortMethods """ Dir Device Name IO hw:1,0,0 UM-1 MIDI 1 IO hw:2,0,0 nanoKONTROL2 MIDI 1 IO hw:2,0,0 MPK mini MIDI 1 """ def get_devices(): devices = [] lines = subprocess.check_output( ["amidi", "-l"], encoding="utf-8", ).splitlines() for line in lines[1:]: mode, device, name = line.strip().split(None, 2) devices.append({'name': name.strip(), 'device': device, 'is_input': 'I' in mode, 'is_output': 'O' in mode, }) return devices def _get_device(name, mode): for dev in get_devices(): if name == dev['name'] and dev[mode]: return dev else: raise OSError(f'unknown port {name!r}') class Input(PortMethods, InputMethods): def __init__(self, name=None, **kwargs): self.name = name self.closed = False self._proc = None self._poller = select.poll() self._lock = threading.RLock() dev = _get_device(self.name, 'is_input') self._proc = subprocess.Popen(['amidi', '-d', '-p', dev['device']], stdout=subprocess.PIPE) self._poller.register(self._proc.stdout, select.POLLIN) def _read_message(self): line = self._proc.stdout.readline().strip().decode('ascii') if line: return Message.from_hex(line) else: # The first line is sometimes blank. return None def receive(self, block=True): if not block: return self.poll() while True: msg = self.poll() if msg: return msg # Wait for message. self._poller.poll() def poll(self): with self._lock: while self._poller.poll(0): msg = self._read_message() if msg is not None: return msg def close(self): if not self.closed: if self._proc: self._proc.kill() self._proc = None self.closed = True class Output(PortMethods, OutputMethods): def __init__(self, name=None, autoreset=False, **kwargs): self.name = name self.autoreset = autoreset self.closed = False self._dev = _get_device(self.name, 'is_output') def send(self, msg): proc = subprocess.Popen(['amidi', '--send-hex', msg.hex(), '-p', self._dev['device']]) proc.wait() def close(self): if not self.closed: if self.autoreset: self.reset() self.closed = True mido-1.3.3/mido/backends/backend.py0000644000175100001770000001517014706731605017733 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2014 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT import importlib import os from .. import ports DEFAULT_BACKEND = 'mido.backends.rtmidi' class Backend: """ Wrapper for backend module. A backend module implements classes for input and output ports for a specific MIDI library. The Backend object wraps around the object and provides convenient 'open_*()' and 'get_*_names()' functions. """ def __init__(self, name=None, api=None, load=False, use_environ=True): self.name = name or os.environ.get('MIDO_BACKEND', DEFAULT_BACKEND) self.api = api self.use_environ = use_environ self._module = None # Split out api (if present). if api: self.api = api elif self.name and '/' in self.name: self.name, self.api = self.name.split('/', 1) else: self.api = None if load: self.load() @property def module(self): """A reference module implementing the backend. This will always be a valid reference to a module. Accessing this property will load the module. Use .loaded to check if the module is loaded. """ self.load() return self._module @property def loaded(self): """Return True if the module is loaded.""" return self._module is not None def load(self): """Load the module. Does nothing if the module is already loaded. This function will be called if you access the 'module' property.""" if not self.loaded: self._module = importlib.import_module(self.name) def _env(self, name): if self.use_environ: return os.environ.get(name) else: return None def _add_api(self, kwargs): if self.api and 'api' not in kwargs: kwargs['api'] = self.api return kwargs def open_input(self, name=None, virtual=False, callback=None, **kwargs): """Open an input port. If the environment variable MIDO_DEFAULT_INPUT is set, it will override the default port. virtual=False Passing True opens a new port that other applications can connect to. Raises IOError if not supported by the backend. callback=None A callback function to be called when a new message arrives. The function should take one argument (the message). Raises IOError if not supported by the backend. """ kwargs.update(dict(virtual=virtual, callback=callback)) if name is None: name = self._env('MIDO_DEFAULT_INPUT') return self.module.Input(name, **self._add_api(kwargs)) def open_output(self, name=None, virtual=False, autoreset=False, **kwargs): """Open an output port. If the environment variable MIDO_DEFAULT_OUTPUT is set, it will override the default port. virtual=False Passing True opens a new port that other applications can connect to. Raises IOError if not supported by the backend. autoreset=False Automatically send all_notes_off and reset_all_controllers on all channels. This is the same as calling `port.reset()`. """ kwargs.update(dict(virtual=virtual, autoreset=autoreset)) if name is None: name = self._env('MIDO_DEFAULT_OUTPUT') return self.module.Output(name, **self._add_api(kwargs)) def open_ioport(self, name=None, virtual=False, callback=None, autoreset=False, **kwargs): """Open a port for input and output. If the environment variable MIDO_DEFAULT_IOPORT is set, it will override the default port. virtual=False Passing True opens a new port that other applications can connect to. Raises IOError if not supported by the backend. callback=None A callback function to be called when a new message arrives. The function should take one argument (the message). Raises IOError if not supported by the backend. autoreset=False Automatically send all_notes_off and reset_all_controllers on all channels. This is the same as calling `port.reset()`. """ kwargs.update(dict(virtual=virtual, callback=callback, autoreset=autoreset)) if name is None: name = self._env('MIDO_DEFAULT_IOPORT') or None if hasattr(self.module, 'IOPort'): # Backend has a native IOPort. Use it. return self.module.IOPort(name, **self._add_api(kwargs)) else: # Backend has no native IOPort. Use the IOPort wrapper # in midi.ports. # # We need an input and an output name. # MIDO_DEFAULT_IOPORT overrides the other two variables. if name: input_name = output_name = name else: input_name = self._env('MIDO_DEFAULT_INPUT') output_name = self._env('MIDO_DEFAULT_OUTPUT') kwargs = self._add_api(kwargs) return ports.IOPort(self.module.Input(input_name, **kwargs), self.module.Output(output_name, **kwargs)) def _get_devices(self, **kwargs): if hasattr(self.module, 'get_devices'): return self.module.get_devices(**self._add_api(kwargs)) else: return [] def get_input_names(self, **kwargs): """Return a list of all input port names.""" devices = self._get_devices(**self._add_api(kwargs)) names = [device['name'] for device in devices if device['is_input']] return names def get_output_names(self, **kwargs): """Return a list of all output port names.""" devices = self._get_devices(**self._add_api(kwargs)) names = [device['name'] for device in devices if device['is_output']] return names def get_ioport_names(self, **kwargs): """Return a list of all I/O port names.""" devices = self._get_devices(**self._add_api(kwargs)) inputs = [device['name'] for device in devices if device['is_input']] outputs = { device['name'] for device in devices if device['is_output']} return [name for name in inputs if name in outputs] def __repr__(self): if self.loaded: status = 'loaded' else: status = 'not loaded' if self.api: name = f'{self.name}/{self.api}' else: name = self.name return f'' mido-1.3.3/mido/backends/portmidi.py0000644000175100001770000002145514706731605020176 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT """ Input and Output ports for PortMidi. There is no need to use this module directly. All you need is available in the toplevel module. PortMidi documentation: http://portmedia.sourceforge.net/portmidi/doxygen/ """ import ctypes import threading from ..ports import BaseInput, BaseOutput, sleep from . import portmidi_init as pm _state = {'port_count': 0} def _refresh_port_list(): if _state['port_count'] == 0: # If no ports are open we can reboot PortMidi # to refresh the port list. This is a hack, but it's # the only way to get an up-to-date list. pm.lib.Pm_Terminate() pm.lib.Pm_Initialize() def _check_error(return_value): """Raise IOError if return_value < 0. The exception will be raised with the error message from PortMidi. """ if return_value == pm.pmHostError: raise OSError('PortMidi Host Error: ' '{}'.format(pm.get_host_error_message())) elif return_value < 0: raise OSError(pm.lib.Pm_GetErrorText(return_value)) def _get_device(device_id): info_pointer = pm.lib.Pm_GetDeviceInfo(device_id) if not info_pointer: raise OSError('PortMidi device with id={} not found'.format( device_id)) info = info_pointer.contents return { 'id': device_id, 'interface': info.interface.decode('utf-8'), 'name': info.name.decode('utf-8'), 'is_input': info.is_input, 'is_output': info.is_output, 'opened': bool(info.opened), } def _get_default_device(get_input): if get_input: device_id = pm.lib.Pm_GetDefaultInputDeviceID() else: device_id = pm.lib.Pm_GetDefaultOutputDeviceID() if device_id < 0: raise OSError('no default port found') return _get_device(device_id) def _get_named_device(name, get_input): # Look for the device by name and type (input / output) for device in get_devices(): if device['name'] != name: continue # Skip if device is the wrong type if get_input: if not device['is_input']: continue else: if not device['is_output']: continue if device['opened']: raise OSError(f'port already opened: {name!r}') return device else: raise OSError(f'unknown port {name!r}') def get_devices(**kwargs): """Return a list of devices as dictionaries.""" _refresh_port_list() return [_get_device(i) for i in range(pm.lib.Pm_CountDevices())] class PortCommon: """ Mixin with common things for input and output ports. """ def _open(self, **kwargs): _refresh_port_list() if kwargs.get('virtual'): raise ValueError('virtual ports are not supported' ' by the PortMidi backend') self._stream = pm.PortMidiStreamPtr() if self.name is None: device = _get_default_device(self.is_input) self.name = device['name'] else: device = _get_named_device(self.name, self.is_input) if device['opened']: if self.is_input: devtype = 'input' else: devtype = 'output' raise OSError('{} port {!r} is already open'.format(devtype, self.name)) if self.is_input: _check_error(pm.lib.Pm_OpenInput( ctypes.byref(self._stream), device['id'], # Input device pm.null, # Input driver info 1000, # Buffer size pm.NullTimeProcPtr, # Time callback pm.null)) # Time info else: _check_error(pm.lib.Pm_OpenOutput( ctypes.byref(self._stream), device['id'], # Output device pm.null, # Output diver info 0, # Buffer size # (ignored when latency == 0?) pm.NullTimeProcPtr, # Default to internal clock pm.null, # Time info 0)) # Latency # This is set when we return, but the callback thread needs # it to be False now (or it will just return right away.) self.closed = False _state['port_count'] += 1 if self.is_input: self._thread = None self.callback = kwargs.get('callback') self._device_type = 'PortMidi/{}'.format(device['interface']) def _close(self): self.callback = None _check_error(pm.lib.Pm_Close(self._stream)) _state['port_count'] -= 1 class Input(PortCommon, BaseInput): """ PortMidi Input port """ def _receive(self, block=True): # Since there is no blocking read in PortMidi, the block # flag is ignored and the enclosing receive() takes care # of blocking. # Allocate buffer. # I get hanging notes if MAX_EVENTS > 1, so I'll have to # resort to calling Pm_Read() in a loop until there are no # more pending events. max_events = 1 BufferType = pm.PmEvent * max_events read_buffer = BufferType() # Read available data from the stream and feed it to the parser. while pm.lib.Pm_Poll(self._stream): # TODO: this should be allocated once # Read one message. Should return 1. # If num_events < 0, an error occured. length = 1 # Buffer length num_events = pm.lib.Pm_Read(self._stream, read_buffer, length) _check_error(num_events) # Get the event event = read_buffer[0] # print('Received: {:x}'.format(event.message)) # The bytes of the message are stored like this: # 0x00201090 -> (0x90, 0x10, 0x10) # (TODO: not sure if this is correct.) packed_message = event.message & 0xffffffff for _i in range(4): byte = packed_message & 0xff self._parser.feed_byte(byte) packed_message >>= 8 @property def callback(self): return self._callback @callback.setter def callback(self, func): self._callback = func if func is None: self._stop_thread() else: self._start_thread() def _start_thread(self): """Start callback thread if not already running.""" if not self._thread: self._stop_event = None self._thread = threading.Thread( target=self._thread_main) self._thread.daemon = True self._thread.start() def _stop_thread(self): """Stop callback thread if running.""" if self._thread: # Ask callback thread to stop. self._stop_event = threading.Event() self._stop_event.wait() self._thread = None def _thread_main(self): # TODO: exceptions do not propagate to the main thread, so if # something goes wrong here there is no way to detect it, and # there is no warning. (An unknown variable, for example, will # just make the thread stop silently.) # Receive messages from port until it's closed # or some exception occurs. try: while not self._stop_event: self._receive() for message in self._parser: if self.callback: self.callback(message) sleep() finally: # Inform parent thread that we are done. if self._stop_event: self._stop_event.set() def _close(self): self._stop_thread() PortCommon._close(self) class Output(PortCommon, BaseOutput): """ PortMidi output port """ def _send(self, message): if message.type == 'sysex': # Sysex messages are written as a string. string = ctypes.c_char_p(bytes(message.bin())) timestamp = 0 # Ignored when latency = 0 _check_error(pm.lib.Pm_WriteSysEx(self._stream, timestamp, string)) else: # The bytes of a message as packed into a 32 bit integer. packed_message = 0 for byte in reversed(message.bytes()): packed_message <<= 8 packed_message |= byte timestamp = 0 # Ignored when latency = 0 _check_error(pm.lib.Pm_WriteShort(self._stream, timestamp, packed_message)) mido-1.3.3/mido/backends/portmidi_init.py0000644000175100001770000001070614706731605021216 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT """ Low-level wrapper for PortMidi library Copied straight from Grant Yoshida's portmidizero, with slight modifications. """ import ctypes import ctypes.util import sys dll_name = '' if sys.platform == 'darwin': dll_name = ctypes.util.find_library('libportmidi.dylib') elif sys.platform in ('win32', 'cygwin'): dll_name = 'portmidi.dll' else: dll_name = 'libportmidi.so' lib = ctypes.CDLL(dll_name) null = None false = 0 true = 1 # portmidi.h # From portmidi.h PM_HOST_ERROR_MSG_LEN = 256 def get_host_error_message(): """Return host error message.""" buf = ctypes.create_string_buffer(PM_HOST_ERROR_MSG_LEN) lib.Pm_GetHostErrorText(buf, PM_HOST_ERROR_MSG_LEN) return buf.raw.decode().rstrip('\0') PmError = ctypes.c_int # PmError enum pmNoError = 0 pmHostError = -10000 pmInvalidDeviceId = -9999 pmInsufficientMemory = -9989 pmBufferTooSmall = -9979 pmBufferOverflow = -9969 pmBadPtr = -9959 pmBadData = -9994 pmInternalError = -9993 pmBufferMaxSize = -9992 lib.Pm_Initialize.restype = PmError lib.Pm_Terminate.restype = PmError PmDeviceID = ctypes.c_int PortMidiStreamPtr = ctypes.c_void_p PmStreamPtr = PortMidiStreamPtr PortMidiStreamPtrPtr = ctypes.POINTER(PortMidiStreamPtr) lib.Pm_HasHostError.restype = ctypes.c_int lib.Pm_HasHostError.argtypes = [PortMidiStreamPtr] lib.Pm_GetErrorText.restype = ctypes.c_char_p lib.Pm_GetErrorText.argtypes = [PmError] lib.Pm_GetHostErrorText.argtypes = [ctypes.c_char_p, ctypes.c_uint] pmNoDevice = -1 class PmDeviceInfo(ctypes.Structure): _fields_ = [("structVersion", ctypes.c_int), ("interface", ctypes.c_char_p), ("name", ctypes.c_char_p), ("is_input", ctypes.c_int), ("is_output", ctypes.c_int), ("opened", ctypes.c_int)] PmDeviceInfoPtr = ctypes.POINTER(PmDeviceInfo) lib.Pm_CountDevices.restype = ctypes.c_int lib.Pm_GetDefaultOutputDeviceID.restype = PmDeviceID lib.Pm_GetDefaultInputDeviceID.restype = PmDeviceID PmTimestamp = ctypes.c_long PmTimeProcPtr = ctypes.CFUNCTYPE(PmTimestamp, ctypes.c_void_p) NullTimeProcPtr = ctypes.cast(null, PmTimeProcPtr) # PmBefore is not defined lib.Pm_GetDeviceInfo.argtypes = [PmDeviceID] lib.Pm_GetDeviceInfo.restype = PmDeviceInfoPtr lib.Pm_OpenInput.restype = PmError lib.Pm_OpenInput.argtypes = [PortMidiStreamPtrPtr, PmDeviceID, ctypes.c_void_p, ctypes.c_long, PmTimeProcPtr, ctypes.c_void_p] lib.Pm_OpenOutput.restype = PmError lib.Pm_OpenOutput.argtypes = [PortMidiStreamPtrPtr, PmDeviceID, ctypes.c_void_p, ctypes.c_long, PmTimeProcPtr, ctypes.c_void_p, ctypes.c_long] lib.Pm_SetFilter.restype = PmError lib.Pm_SetFilter.argtypes = [PortMidiStreamPtr, ctypes.c_long] lib.Pm_SetChannelMask.restype = PmError lib.Pm_SetChannelMask.argtypes = [PortMidiStreamPtr, ctypes.c_int] lib.Pm_Abort.restype = PmError lib.Pm_Abort.argtypes = [PortMidiStreamPtr] lib.Pm_Close.restype = PmError lib.Pm_Close.argtypes = [PortMidiStreamPtr] PmMessage = ctypes.c_long class PmEvent(ctypes.Structure): _fields_ = [("message", PmMessage), ("timestamp", PmTimestamp)] PmEventPtr = ctypes.POINTER(PmEvent) lib.Pm_Read.restype = PmError lib.Pm_Read.argtypes = [PortMidiStreamPtr, PmEventPtr, ctypes.c_long] lib.Pm_Poll.restype = PmError lib.Pm_Poll.argtypes = [PortMidiStreamPtr] lib.Pm_Write.restype = PmError lib.Pm_Write.argtypes = [PortMidiStreamPtr, PmEventPtr, ctypes.c_long] lib.Pm_WriteShort.restype = PmError lib.Pm_WriteShort.argtypes = [PortMidiStreamPtr, PmTimestamp, ctypes.c_long] lib.Pm_WriteSysEx.restype = PmError lib.Pm_WriteSysEx.argtypes = [PortMidiStreamPtr, PmTimestamp, ctypes.c_char_p] # porttime.h # PtError enum PtError = ctypes.c_int ptNoError = 0 ptHostError = -10000 ptAlreadyStarted = -9999 ptAlreadyStopped = -9998 ptInsufficientMemory = -9997 PtTimestamp = ctypes.c_long PtCallback = ctypes.CFUNCTYPE(PmTimestamp, ctypes.c_void_p) lib.Pt_Start.restype = PtError lib.Pt_Start.argtypes = [ctypes.c_int, PtCallback, ctypes.c_void_p] lib.Pt_Stop.restype = PtError lib.Pt_Started.restype = ctypes.c_int lib.Pt_Time.restype = PtTimestamp mido-1.3.3/mido/backends/pygame.py0000644000175100001770000000725214706731605017630 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT """ Mido ports for pygame.midi. Pygame uses PortMidi, so this is perhaps not very useful. http://www.pygame.org/docs/ref/midi.html """ from pygame import midi from ..ports import BaseInput, BaseOutput def _get_device(device_id): keys = ['interface', 'name', 'is_input', 'is_output', 'opened'] info = dict(zip(keys, midi.get_device_info(device_id))) # TODO: correct encoding? info['name'] = info['name'].decode('utf-8') info['id'] = device_id return info def _get_default_device(get_input): if get_input: device_id = midi.get_default_input_id() else: device_id = midi.get_default_output_id() if device_id < 0: raise OSError('no default port found') return _get_device(device_id) def _get_named_device(name, get_input): # Look for the device by name and type (input / output) for device in get_devices(): if device['name'] != name: continue # Skip if device is the wrong type if get_input: if device['is_output']: continue else: if device['is_input']: continue if device['opened']: raise OSError(f'port already opened: {name!r}') return device else: raise OSError(f'unknown port: {name!r}') def get_devices(**kwargs): midi.init() return [_get_device(device_id) for device_id in range(midi.get_count())] class PortCommon: """ Mixin with common things for input and output ports. """ def _open(self, **kwargs): if kwargs.get('virtual'): raise ValueError('virtual ports are not supported' ' by the Pygame backend') elif kwargs.get('callback'): raise ValueError('callbacks are not supported' ' by the Pygame backend') midi.init() if self.name is None: device = _get_default_device(self.is_input) self.name = device['name'] else: device = _get_named_device(self.name, self.is_input) if device['opened']: if self.is_input: devtype = 'input' else: devtype = 'output' raise OSError('{} port {!r} is already open'.format(devtype, self.name)) if self.is_input: self._port = midi.Input(device['id']) else: self._port = midi.Output(device['id']) self._device_type = 'Pygame/{}'.format(device['interface']) def _close(self): self._port.close() class Input(PortCommon, BaseInput): """ PortMidi Input port """ def _receive(self, block=True): # I get hanging notes if MAX_EVENTS > 1, so I'll have to # resort to calling Pm_Read() in a loop until there are no # more pending events. while self._port.poll(): bytes, time = self._port.read(1)[0] self._parser.feed(bytes) class Output(PortCommon, BaseOutput): """ PortMidi output port """ def _send(self, message): if message.type == 'sysex': # Python 2 version of Pygame accepts a bytes or list here # while Python 3 version requires bytes. # According to the docs it should accept both so this may be # a bug in Pygame: # https://www.pygame.org/docs/ref/midi.html#pygame.midi.Output.write_sys_ex self._port.write_sys_ex(midi.time(), bytes(message.bin())) else: self._port.write_short(*message.bytes()) mido-1.3.3/mido/backends/rtmidi.py0000644000175100001770000001303214706731605017627 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT """Backend for python-rtmidi: http://pypi.python.org/pypi/python-rtmidi/ """ import threading import rtmidi from .. import ports from ..messages import Message from ._parser_queue import ParserQueue from .rtmidi_utils import expand_alsa_port_name def _get_api_lookup(): api_to_name = {} name_to_api = {} for name in dir(rtmidi): if name.startswith('API_'): value = getattr(rtmidi, name) name = name.replace('API_', '') name_to_api[name] = value api_to_name[value] = name return api_to_name, name_to_api _api_to_name, _name_to_api = _get_api_lookup() def _get_api_id(name=None): if name is None: return rtmidi.API_UNSPECIFIED try: api = _name_to_api[name] except KeyError as ke: raise ValueError(f'unknown API {name}') from ke if name in get_api_names(): return api else: raise ValueError(f'API {name} not compiled in') def get_devices(api=None, **kwargs): devices = {} rtapi = _get_api_id(api) mi = rtmidi.MidiIn(rtapi=rtapi) mo = rtmidi.MidiOut(rtapi=rtapi) input_names = mi.get_ports() output_names = mo.get_ports() for name in input_names + output_names: if name not in devices: devices[name] = { 'name': name, 'is_input': name in input_names, 'is_output': name in output_names, } mi.delete() mo.delete() return list(devices.values()) def get_api_names(): return [_api_to_name[n] for n in rtmidi.get_compiled_api()] def _open_port(rt, name=None, client_name=None, virtual=False, api=None): if client_name is not None: virtual = True if virtual: if name is None: raise OSError('virtual port must have a name') rt.open_virtual_port(name) return name if api == 'LINUX_ALSA': name = expand_alsa_port_name(rt.get_ports(), name) port_names = rt.get_ports() if len(port_names) == 0: raise OSError('no ports available') if name is None: name = port_names[0] port_id = 0 elif name in port_names: port_id = port_names.index(name) else: raise OSError(f'unknown port {name!r}') try: rt.open_port(port_id) except RuntimeError as err: raise OSError(*err.args) from err return name class PortCommon: def _close(self): self._rt.close_port() self._rt.delete() class Input(PortCommon, ports.BaseInput): _locking = False def _open(self, client_name=None, virtual=False, api=None, callback=None, **kwargs): self.closed = True self._callback_lock = threading.RLock() self._queue = ParserQueue() rtapi = _get_api_id(api) self._rt = rtmidi.MidiIn(name=client_name, rtapi=rtapi) self.api = _api_to_name[self._rt.get_current_api()] self._device_type = f'RtMidi/{self.api}' self.name = _open_port(self._rt, self.name, client_name=client_name, virtual=virtual, api=self.api) self._rt.ignore_types(False, False, True) self.closed = False # We need to do this last when everything is set up. self.callback = callback def _close(self): # Deregister the callback before closing the port # to prevent crashing another thread. self.callback = None super()._close() # We override receive() and poll() instead of _receive() and # _poll() to bypass locking. def receive(self, block=True): if block: return self._queue.get() else: return self._queue.poll() def poll(self): return self._queue.poll() receive.__doc__ = ports.BaseInput.receive.__doc__ poll.__doc__ = ports.BaseInput.poll.__doc__ @property def callback(self): return self._callback @callback.setter def callback(self, func): with self._callback_lock: # Make sure the callback doesn't run while were swapping it out. self._rt.cancel_callback() if func: # Make sure the callback gets all the queued messages. for msg in self._queue.iterpoll(): func(msg) self._callback = func self._rt.set_callback(self._callback_wrapper) def _callback_wrapper(self, msg_data, data): try: msg = Message.from_bytes(msg_data[0]) except ValueError: # Ignore invalid message. return (self._callback or self._queue.put)(msg) class Output(PortCommon, ports.BaseOutput): _locking = False def _open(self, client_name=None, virtual=False, api=None, callback=None, **kwargs): self.closed = True self._send_lock = threading.RLock() rtapi = _get_api_id(api) self._rt = rtmidi.MidiOut(name=client_name, rtapi=rtapi) self.api = _api_to_name[self._rt.get_current_api()] self._device_type = f'RtMidi/{self.api}' self.name = _open_port(self._rt, self.name, client_name=client_name, virtual=virtual, api=self.api) self.closed = False # We override send() instead of _send() to bypass locking. def send(self, msg): """Send a message on the port.""" with self._send_lock: self._rt.send_message(msg.bytes()) send.__doc__ = ports.BaseOutput.send.__doc__ mido-1.3.3/mido/backends/rtmidi_python.py0000644000175100001770000000746314706731605021243 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT """Backend for rtmidi-python: https://pypi.python.org/pypi/rtmidi-python To use this backend copy (or link) it to somewhere in your Python path and call: mido.set_backend('mido.backends.rtmidi_python') or set shell variable $MIDO_BACKEND to mido.backends.rtmidi_python TODO: * add support for APIs. * active_sensing is still filtered. (The same is true for mido.backends.rtmidi.)There may be a way to remove this filtering. """ import queue import rtmidi_python as rtmidi # TODO: change this to a relative import if the backend is included in # the package. from ..ports import BaseInput, BaseOutput def get_devices(api=None, **kwargs): devices = {} input_names = rtmidi.MidiIn().ports output_names = rtmidi.MidiOut().ports for name in input_names + output_names: if name not in devices: devices[name] = { 'name': name, 'is_input': name in input_names, 'is_output': name in output_names, } return list(devices.values()) class PortCommon: def _open(self, virtual=False, **kwargs): self._queue = queue.Queue() self._callback = None # rtapi = _get_api_id(api) opening_input = hasattr(self, 'receive') if opening_input: self._rt = rtmidi.MidiIn() self._rt.ignore_types(False, False, True) self.callback = kwargs.get('callback') else: self._rt = rtmidi.MidiOut() # rtapi=rtapi) # Turn of ignore of sysex, time and active_sensing. ports = self._rt.ports if virtual: if self.name is None: raise OSError('virtual port must have a name') self._rt.open_virtual_port(self.name) else: if self.name is None: # TODO: this could fail if list is empty. # In RtMidi, the default port is the first port. try: self.name = ports[0] except IndexError as ie: raise OSError('no ports available') from ie try: port_id = ports.index(self.name) except ValueError as ve: raise OSError(f'unknown port {self.name!r}') from ve try: self._rt.open_port(port_id) except RuntimeError as err: raise OSError(*err.args) from err # api = _api_to_name[self._rt.get_current_api()] api = '' self._device_type = f'RtMidi/{api}' if virtual: self._device_type = f'virtual {self._device_type}' @property def callback(self): return self._callback @callback.setter def callback(self, func): self._callback = func if func is None: self._rt.callback = None else: self._rt.callback = self._callback_wrapper def _callback_wrapper(self, msg_bytes, timestamp): self._parser.feed(msg_bytes) for message in self._parser: if self.callback: self.callback(message) def _close(self): self._rt.close_port() del self._rt # Virtual ports are closed when this is deleted. class Input(PortCommon, BaseInput): def _receive(self, block=True): # Since there is no blocking read in RtMidi, the block # flag is ignored and the enclosing receive() takes care # of blocking. while True: message_data, timestamp = self._rt.get_message() if message_data is None: break else: self._parser.feed(message_data) class Output(PortCommon, BaseOutput): def _send(self, message): self._rt.send_message(message.bytes()) mido-1.3.3/mido/backends/rtmidi_utils.py0000644000175100001770000000266314706731605021057 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2017 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT """Utility functions for RtMidi backend. These are in a separate file so they can be tested without the `python-rtmidi` package. """ def expand_alsa_port_name(port_names, name): """Expand ALSA port name. RtMidi/ALSA includes client name and client:port number in the port name, for example: TiMidity:TiMidity port 0 128:0 This allows you to specify only port name or client:port name when opening a port. It will compare the name to each name in port_names (typically returned from get_*_names()) and try these three variants in turn: TiMidity:TiMidity port 0 128:0 TiMidity:TiMidity port 0 TiMidity port 0 It returns the first match. If no match is found it returns the passed name so the caller can deal with it. """ if name is None: return None for port_name in port_names: if name == port_name: return name # Try without client and port number (for example 128:0). without_numbers = port_name.rsplit(None, 1)[0] if name == without_numbers: return port_name if ':' in without_numbers: without_client = without_numbers.split(':', 1)[1] if name == without_client: return port_name else: # Let caller deal with it. return name mido-1.3.3/mido/frozen.py0000644000175100001770000000454314706731605016077 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2016 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT from .messages import Message from .midifiles import MetaMessage, UnknownMetaMessage class Frozen: def __setattr__(self, *_): raise ValueError('frozen message is immutable') def __hash__(self): return hash(tuple(sorted(vars(self).items()))) class FrozenMessage(Frozen, Message): pass class FrozenMetaMessage(Frozen, MetaMessage): pass class FrozenUnknownMetaMessage(Frozen, UnknownMetaMessage): def __repr__(self): return 'Frozen' + UnknownMetaMessage.__repr__(self) def is_frozen(msg): """Return True if message is frozen, otherwise False.""" return isinstance(msg, Frozen) # TODO: these two functions are almost the same except inverted. There # should be a way to refactor them to lessen code duplication. def freeze_message(msg): """Freeze message. Returns a frozen version of the message. Frozen messages are immutable, hashable and can be used as dictionary keys. Will return None if called with None. This allows you to do things like:: msg = freeze_message(port.poll()) """ if isinstance(msg, Frozen): # Already frozen. return msg elif isinstance(msg, Message): class_ = FrozenMessage elif isinstance(msg, UnknownMetaMessage): class_ = FrozenUnknownMetaMessage elif isinstance(msg, MetaMessage): class_ = FrozenMetaMessage elif msg is None: return None else: raise ValueError('first argument must be a message or None') frozen = class_.__new__(class_) vars(frozen).update(vars(msg)) return frozen def thaw_message(msg): """Thaw message. Returns a mutable version of a frozen message. Will return None if called with None. """ if not isinstance(msg, Frozen): # Already thawed, just return a copy. return msg.copy() elif isinstance(msg, FrozenMessage): class_ = Message elif isinstance(msg, FrozenUnknownMetaMessage): class_ = UnknownMetaMessage elif isinstance(msg, FrozenMetaMessage): class_ = MetaMessage elif msg is None: return None else: raise ValueError('first argument must be a message or None') thawed = class_.__new__(class_) vars(thawed).update(vars(msg)) return thawed mido-1.3.3/mido/messages/0000755000175100001770000000000014706731615016024 5ustar runnerdocker00000000000000mido-1.3.3/mido/messages/__init__.py0000644000175100001770000000126514706731605020140 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2016 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT from .checks import check_time from .messages import ( BaseMessage, Message, format_as_string, parse_string, parse_string_stream, ) from .specs import ( MAX_PITCHWHEEL, MAX_SONGPOS, MIN_PITCHWHEEL, MIN_SONGPOS, SPEC_BY_STATUS, SPEC_BY_TYPE, SPEC_LOOKUP, ) __all__ = [ "BaseMessage", "MAX_PITCHWHEEL", "MAX_SONGPOS", "MIN_PITCHWHEEL", "MIN_SONGPOS", "Message", "SPEC_BY_STATUS", "SPEC_BY_TYPE", "SPEC_LOOKUP", "check_time", "format_as_string", "parse_string", "parse_string_stream", ] mido-1.3.3/mido/messages/checks.py0000644000175100001770000000553014706731605017640 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2016 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT from numbers import Integral, Real from .specs import ( MAX_PITCHWHEEL, MAX_SONGPOS, MIN_PITCHWHEEL, MIN_SONGPOS, SPEC_BY_TYPE, ) def check_type(type_): if type_ not in SPEC_BY_TYPE: raise ValueError(f'invalid message type {type_!r}') def check_channel(channel): if not isinstance(channel, Integral): raise TypeError('channel must be int') elif not 0 <= channel <= 15: raise ValueError('channel must be in range 0..15') def check_pos(pos): if not isinstance(pos, Integral): raise TypeError('song pos must be int') elif not MIN_SONGPOS <= pos <= MAX_SONGPOS: raise ValueError('song pos must be in range {}..{}'.format( MIN_SONGPOS, MAX_SONGPOS)) def check_pitch(pitch): if not isinstance(pitch, Integral): raise TypeError('pichwheel value must be int') elif not MIN_PITCHWHEEL <= pitch <= MAX_PITCHWHEEL: raise ValueError('pitchwheel value must be in range {}..{}'.format( MIN_PITCHWHEEL, MAX_PITCHWHEEL)) def check_data(data_bytes): for byte in data_bytes: check_data_byte(byte) def check_frame_type(value): if not isinstance(value, Integral): raise TypeError('frame_type must be int') elif not 0 <= value <= 7: raise ValueError('frame_type must be in range 0..7') def check_frame_value(value): if not isinstance(value, Integral): raise TypeError('frame_value must be int') elif not 0 <= value <= 15: raise ValueError('frame_value must be in range 0..15') def check_data_byte(value): if not isinstance(value, Integral): raise TypeError('data byte must be int') elif not 0 <= value <= 127: raise ValueError('data byte must be in range 0..127') def check_time(time): if not isinstance(time, Real): raise TypeError('time must be int or float') _CHECKS = { 'channel': check_channel, 'control': check_data_byte, 'data': check_data, 'frame_type': check_frame_type, 'frame_value': check_frame_value, 'note': check_data_byte, 'pitch': check_pitch, 'pos': check_pos, 'program': check_data_byte, 'song': check_data_byte, 'time': check_time, 'type': check_type, 'value': check_data_byte, 'velocity': check_data_byte, } def check_value(name, value): _CHECKS[name](value) def check_msgdict(msgdict): spec = SPEC_BY_TYPE.get(msgdict['type']) if spec is None: raise ValueError('unknown message type {!r}'.format(msgdict['type'])) for name, value in msgdict.items(): if name not in spec['attribute_names']: raise ValueError( '{} message has no attribute {}'.format(spec['type'], name)) check_value(name, value) mido-1.3.3/mido/messages/decode.py0000644000175100001770000000532214706731605017622 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2016 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT from .checks import check_data from .specs import ( CHANNEL_MESSAGES, MIN_PITCHWHEEL, SPEC_BY_STATUS, SYSEX_END, SYSEX_START, ) def _decode_sysex_data(data): return {'data': tuple(data)} def _decode_quarter_frame_data(data): return {'frame_type': data[0] >> 4, 'frame_value': data[0] & 15} def _decode_songpos_data(data): return {'pos': data[0] | (data[1] << 7)} def _decode_pitchwheel_data(data): return {'pitch': data[0] | ((data[1] << 7) + MIN_PITCHWHEEL)} def _make_special_cases(): cases = { 0xe0: _decode_pitchwheel_data, 0xf0: _decode_sysex_data, 0xf1: _decode_quarter_frame_data, 0xf2: _decode_songpos_data, } for i in range(16): cases[0xe0 | i] = _decode_pitchwheel_data return cases _SPECIAL_CASES = _make_special_cases() def _decode_data_bytes(status_byte, data, spec): # Subtract 1 for status byte. if len(data) != (spec['length'] - 1): raise ValueError( 'wrong number of bytes for {} message'.format(spec['type'])) # TODO: better name than args? names = [name for name in spec['value_names'] if name != 'channel'] args = {name: value for name, value in zip(names, data)} if status_byte in CHANNEL_MESSAGES: # Channel is stored in the lower nibble of the status byte. args['channel'] = status_byte & 0x0f return args def decode_message(msg_bytes, time=0, check=True): """Decode message bytes and return messages as a dictionary. Raises ValueError if the bytes are out of range or the message is invalid. This is not a part of the public API. """ # TODO: this function is getting long. if len(msg_bytes) == 0: raise ValueError('message is 0 bytes long') status_byte = msg_bytes[0] data = msg_bytes[1:] try: spec = SPEC_BY_STATUS[status_byte] except KeyError as ke: raise ValueError(f'invalid status byte {status_byte!r}') from ke msg = { 'type': spec['type'], 'time': time, } # Sysex. if status_byte == SYSEX_START: if len(data) < 1: raise ValueError('sysex without end byte') end = data[-1] data = data[:-1] if end != SYSEX_END: raise ValueError(f'invalid sysex end byte {end!r}') if check: check_data(data) if status_byte in _SPECIAL_CASES: if status_byte in CHANNEL_MESSAGES: msg['channel'] = status_byte & 0x0f msg.update(_SPECIAL_CASES[status_byte](data)) else: msg.update(_decode_data_bytes(status_byte, data, spec)) return msg mido-1.3.3/mido/messages/encode.py0000644000175100001770000000342014706731605017631 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2016 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT from .specs import CHANNEL_MESSAGES, MIN_PITCHWHEEL, SPEC_BY_TYPE def _encode_pitchwheel(msg): pitch = msg['pitch'] - MIN_PITCHWHEEL return [0xe0 | msg['channel'], pitch & 0x7f, pitch >> 7] def _encode_sysex(msg): return [0xf0] + list(msg['data']) + [0xf7] def _encode_quarter_frame(msg): return [0xf1, msg['frame_type'] << 4 | msg['frame_value']] def _encode_songpos(data): pos = data['pos'] return [0xf2, pos & 0x7f, pos >> 7] def _encode_note_off(msg): return [0x80 | msg['channel'], msg['note'], msg['velocity']] def _encode_note_on(msg): return [0x90 | msg['channel'], msg['note'], msg['velocity']] def _encode_control_change(msg): return [0xb0 | msg['channel'], msg['control'], msg['value']] _SPECIAL_CASES = { 'pitchwheel': _encode_pitchwheel, 'sysex': _encode_sysex, 'quarter_frame': _encode_quarter_frame, 'songpos': _encode_songpos, # These are so common that they get special cases to speed things up. 'note_off': _encode_note_off, 'note_on': _encode_note_on, 'control_change': _encode_control_change, } def encode_message(msg): """Encode msg dict as a list of bytes. TODO: Add type and value checking. (Can be turned off with keyword argument.) This is not a part of the public API. """ encode = _SPECIAL_CASES.get(msg['type']) if encode: return encode(msg) else: spec = SPEC_BY_TYPE[msg['type']] status_byte = spec['status_byte'] if status_byte in CHANNEL_MESSAGES: status_byte |= msg['channel'] data = [msg[name] for name in spec['value_names'] if name != 'channel'] return [status_byte] + data mido-1.3.3/mido/messages/messages.py0000644000175100001770000001772314706731605020216 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2016 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT import re from .checks import check_data, check_msgdict, check_value from .decode import decode_message from .encode import encode_message from .specs import REALTIME_TYPES, SPEC_BY_TYPE, make_msgdict from .strings import msg2str, str2msg class BaseMessage: """Abstract base class for messages.""" is_meta = False def copy(self): raise NotImplementedError def bytes(self): raise NotImplementedError def bin(self): """Encode message and return as a bytearray. This can be used to write the message to a file. """ return bytearray(self.bytes()) def hex(self, sep=' '): """Encode message and return as a string of hex numbers, Each number is separated by the string sep. """ return sep.join(f'{byte:02X}' for byte in self.bytes()) def dict(self): """Returns a dictionary containing the attributes of the message. Example: {'type': 'sysex', 'data': [1, 2], 'time': 0} Sysex data will be returned as a list. """ data = vars(self).copy() if data['type'] == 'sysex': # Make sure we return a list instead of a SysexData object. data['data'] = list(data['data']) return data @classmethod def from_dict(cls, data): """Create a message from a dictionary. Only "type" is required. The other will be set to default values. """ return cls(**data) def _get_value_names(self): # This is overridden by MetaMessage. return list(SPEC_BY_TYPE[self.type]['value_names']) + ['time'] def __repr__(self): items = [repr(self.type)] for name in self._get_value_names(): items.append(f'{name}={getattr(self, name)!r}') return '{}({})'.format(type(self).__name__, ', '.join(items)) @property def is_realtime(self): """True if the message is a system realtime message.""" return self.type in REALTIME_TYPES def is_cc(self, control=None): """Return True if the message is of type 'control_change'. The optional control argument can be used to test for a specific control number, for example: if msg.is_cc(7): # Message is control change 7 (channel volume). """ if self.type != 'control_change': return False elif control is None: return True else: return self.control == control def __delattr__(self, name): raise AttributeError('attribute cannot be deleted') def __setattr__(self, name, value): raise AttributeError('message is immutable') def __eq__(self, other): if not isinstance(other, BaseMessage): raise TypeError(f'can\'t compare message to {type(other)}') # This includes time in comparison. return vars(self) == vars(other) class SysexData(tuple): """Special kind of tuple accepts and converts any sequence in +=.""" def __iadd__(self, other): check_data(other) return self + SysexData(other) class Message(BaseMessage): def __init__(self, type, skip_checks=False, **args): msgdict = make_msgdict(type, args) if type == 'sysex': msgdict['data'] = SysexData(msgdict['data']) if not skip_checks: check_msgdict(msgdict) vars(self).update(msgdict) def copy(self, skip_checks=False, **overrides): """Return a copy of the message. Attributes will be overridden by the passed keyword arguments. Only message specific attributes can be overridden. The message type can not be changed. The skip_checks arg can be used to bypass validation of message attributes and should be used cautiously. """ if not overrides: # Bypass all checks. msg = self.__class__.__new__(self.__class__) vars(msg).update(vars(self)) return msg if 'type' in overrides and overrides['type'] != self.type: raise ValueError('copy must be same message type') if 'data' in overrides: overrides['data'] = bytearray(overrides['data']) msgdict = vars(self).copy() msgdict.update(overrides) if not skip_checks: check_msgdict(msgdict) return self.__class__(skip_checks=skip_checks, **msgdict) @classmethod def from_bytes(cl, data, time=0): """Parse a byte encoded message. Accepts a byte string or any iterable of integers. This is the reverse of msg.bytes() or msg.bin(). """ msg = cl.__new__(cl) msgdict = decode_message(data, time=time) if 'data' in msgdict: msgdict['data'] = SysexData(msgdict['data']) vars(msg).update(msgdict) return msg @classmethod def from_hex(cl, text, time=0, sep=None): """Parse a hex encoded message. This is the reverse of msg.hex(). """ # bytearray.fromhex() is a bit picky about its input # so we need to replace all whitespace characters with spaces. text = re.sub(r'\s', ' ', text) if sep is not None: # We also replace the separator with spaces making sure # the string length remains the same so char positions will # be correct in bytearray.fromhex() error messages. text = text.replace(sep, ' ' * len(sep)) return cl.from_bytes(bytearray.fromhex(text), time=time) @classmethod def from_str(cl, text): """Parse a string encoded message. This is the reverse of str(msg). """ return cl(**str2msg(text)) def __len__(self): if self.type == 'sysex': return 2 + len(self.data) else: return SPEC_BY_TYPE[self.type]['length'] def __str__(self): return msg2str(vars(self)) def _setattr(self, name, value): if name == 'type': raise AttributeError('type attribute is read only') elif name not in vars(self): raise AttributeError('{} message has no ' 'attribute {}'.format(self.type, name)) else: check_value(name, value) if name == 'data': vars(self)['data'] = SysexData(value) else: vars(self)[name] = value __setattr__ = _setattr def bytes(self): """Encode message and return as a list of integers.""" return encode_message(vars(self)) def parse_string(text): """Parse a string of text and return a message. The string can span multiple lines, but must contain one full message. Raises ValueError if the string could not be parsed. """ return Message.from_str(text) def parse_string_stream(stream): """Parse a stream of messages and yield (message, error_message) stream can be any iterable that generates text strings, where each string is a string encoded message. If a string can be parsed, (message, None) is returned. If it can't be parsed, (None, error_message) is returned. The error message contains the line number where the error occurred. """ line_number = 1 for line in stream: try: line = line.split('#')[0].strip() if line: yield parse_string(line), None except ValueError as exception: error_message = 'line {line_number}: {msg}'.format( line_number=line_number, msg=exception.args[0]) yield None, error_message line_number += 1 def format_as_string(msg, include_time=True): """Format a message and return as a string. This is equivalent to str(message). To leave out the time attribute, pass include_time=False. """ return msg2str(vars(msg), include_time=include_time) mido-1.3.3/mido/messages/specs.py0000644000175100001770000000662414706731605017522 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2016 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT """Definitions and lookup tables for MIDI messages. TODO: * add lookup functions for messages definitions by type and status byte. """ # TODO: these include undefined messages. CHANNEL_MESSAGES = set(range(0x80, 0xf0)) COMMON_MESSAGES = set(range(0xf0, 0xf8)) REALTIME_MESSAGES = set(range(0xf8, 0x100)) SYSEX_START = 0xf0 SYSEX_END = 0xf7 # Pitchwheel is a 14 bit signed integer MIN_PITCHWHEEL = -8192 MAX_PITCHWHEEL = 8191 # Song pos is a 14 bit unsigned integer MIN_SONGPOS = 0 MAX_SONGPOS = 16383 def _defmsg(status_byte, type_, value_names, length): return { 'status_byte': status_byte, 'type': type_, 'value_names': value_names, 'attribute_names': set(value_names) | {'type', 'time'}, 'length': length, } SPECS = [ _defmsg(0x80, 'note_off', ('channel', 'note', 'velocity'), 3), _defmsg(0x90, 'note_on', ('channel', 'note', 'velocity'), 3), _defmsg(0xa0, 'polytouch', ('channel', 'note', 'value'), 3), _defmsg(0xb0, 'control_change', ('channel', 'control', 'value'), 3), _defmsg(0xc0, 'program_change', ('channel', 'program',), 2), _defmsg(0xd0, 'aftertouch', ('channel', 'value',), 2), _defmsg(0xe0, 'pitchwheel', ('channel', 'pitch',), 3), # System common messages. # 0xf4 and 0xf5 are undefined. _defmsg(0xf0, 'sysex', ('data',), float('inf')), _defmsg(0xf1, 'quarter_frame', ('frame_type', 'frame_value'), 2), _defmsg(0xf2, 'songpos', ('pos',), 3), _defmsg(0xf3, 'song_select', ('song',), 2), _defmsg(0xf6, 'tune_request', (), 1), # System real time messages. # 0xf9 and 0xfd are undefined. _defmsg(0xf8, 'clock', (), 1), _defmsg(0xfa, 'start', (), 1), _defmsg(0xfb, 'continue', (), 1), _defmsg(0xfc, 'stop', (), 1), _defmsg(0xfe, 'active_sensing', (), 1), _defmsg(0xff, 'reset', (), 1), ] def _make_spec_lookups(specs): lookup = {} by_status = {} by_type = {} for spec in specs: type_ = spec['type'] status_byte = spec['status_byte'] by_type[type_] = spec if status_byte in CHANNEL_MESSAGES: for channel in range(16): by_status[status_byte | channel] = spec else: by_status[status_byte] = spec lookup.update(by_status) lookup.update(by_type) return lookup, by_status, by_type SPEC_LOOKUP, SPEC_BY_STATUS, SPEC_BY_TYPE = _make_spec_lookups(SPECS) REALTIME_TYPES = {'tune_request', 'clock', 'start', 'continue', 'stop'} DEFAULT_VALUES = { 'channel': 0, 'control': 0, 'data': (), 'frame_type': 0, 'frame_value': 0, 'note': 0, 'pitch': 0, 'pos': 0, 'program': 0, 'song': 0, 'value': 0, 'velocity': 64, 'time': 0, } # TODO: should this be in decode.py? def make_msgdict(type_, overrides): """Return a new message. Returns a dictionary representing a message. Message values can be overriden. No type or value checking is done. The caller is responsible for calling check_msgdict(). """ if type_ in SPEC_BY_TYPE: spec = SPEC_BY_TYPE[type_] else: raise LookupError(f'Unknown message type {type_!r}') msg = {'type': type_, 'time': DEFAULT_VALUES['time']} for name in spec['value_names']: msg[name] = DEFAULT_VALUES[name] msg.update(overrides) return msg mido-1.3.3/mido/messages/strings.py0000644000175100001770000000325614706731605020074 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2016 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT from .specs import SPEC_BY_TYPE, make_msgdict def msg2str(msg, include_time=True): type_ = msg['type'] spec = SPEC_BY_TYPE[type_] words = [type_] for name in spec['value_names']: value = msg[name] if name == 'data': value = '({})'.format(','.join(str(byte) for byte in value)) words.append(f'{name}={value}') if include_time: words.append('time={}'.format(msg['time'])) return str.join(' ', words) def _parse_time(value): # Convert to int if possible. try: return int(value) except ValueError: pass try: return float(value) except ValueError: pass raise ValueError(f'invalid time {value!r}') def _parse_data(value): if not value.startswith('(') and value.endswith(')'): raise ValueError('missing parentheses in data message') try: return [int(byte) for byte in value[1:-1].split(',')] except ValueError as ve: raise ValueError('unable to parse data bytes') from ve def str2msg(text): """Parse str format and return message dict. No type or value checking is done. The caller is responsible for calling check_msgdict(). """ words = text.split() type_ = words[0] args = words[1:] msg = {} for arg in args: name, value = arg.split('=', 1) if name == 'time': value = _parse_time(value) elif name == 'data': value = _parse_data(value) else: value = int(value) msg[name] = value return make_msgdict(type_, msg) mido-1.3.3/mido/midifiles/0000755000175100001770000000000014706731615016162 5ustar runnerdocker00000000000000mido-1.3.3/mido/midifiles/__init__.py0000644000175100001770000000102114706731605020264 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2016 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT from .meta import KeySignatureError, MetaMessage, UnknownMetaMessage from .midifiles import MidiFile from .tracks import MidiTrack, merge_tracks from .units import bpm2tempo, second2tick, tempo2bpm, tick2second __all__ = [ "KeySignatureError", "MetaMessage", "MidiFile", "MidiTrack", "UnknownMetaMessage", "bpm2tempo", "merge_tracks", "second2tick", "tempo2bpm", "tick2second", ] mido-1.3.3/mido/midifiles/meta.py0000644000175100001770000004007014706731605017462 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2016 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT """ Meta messages for MIDI files. TODO: - what if an unknown meta message is implemented and someone depends on the 'data' attribute? - is 'type_byte' a good name? - 'values' is not a good name for a dictionary. - type and value safety? - copy(). - expose _key_signature_encode/decode? """ import math import struct from contextlib import contextmanager from numbers import Integral from ..messages import BaseMessage, check_time _charset = 'latin1' class KeySignatureError(Exception): """ Raised when key cannot be converted from key/mode to key letter """ pass def _reverse_table(table): """Return value: key for dictionary.""" return {value: key for (key, value) in table.items()} _key_signature_decode = {(-7, 0): 'Cb', (-6, 0): 'Gb', (-5, 0): 'Db', (-4, 0): 'Ab', (-3, 0): 'Eb', (-2, 0): 'Bb', (-1, 0): 'F', (0, 0): 'C', (1, 0): 'G', (2, 0): 'D', (3, 0): 'A', (4, 0): 'E', (5, 0): 'B', (6, 0): 'F#', (7, 0): 'C#', (-7, 1): 'Abm', (-6, 1): 'Ebm', (-5, 1): 'Bbm', (-4, 1): 'Fm', (-3, 1): 'Cm', (-2, 1): 'Gm', (-1, 1): 'Dm', (0, 1): 'Am', (1, 1): 'Em', (2, 1): 'Bm', (3, 1): 'F#m', (4, 1): 'C#m', (5, 1): 'G#m', (6, 1): 'D#m', (7, 1): 'A#m', } _key_signature_encode = _reverse_table(_key_signature_decode) _smpte_framerate_decode = {0: 24, 1: 25, 2: 29.97, 3: 30, } _smpte_framerate_encode = _reverse_table(_smpte_framerate_decode) def signed(to_type, n): formats = {'byte': 'Bb', 'short': 'Hh', 'long': 'Ll', 'ubyte': 'bB', 'ushort': 'hH', 'ulong': 'lL' } try: pack_format, unpack_format = formats[to_type] except KeyError as ke: raise ValueError(f'invalid integer type {to_type}') from ke try: packed = struct.pack(pack_format, n) return struct.unpack(unpack_format, packed)[0] except struct.error as err: raise ValueError(*err.args) from err def unsigned(to_type, n): return signed(f'u{to_type}', n) def encode_variable_int(value): """Encode variable length integer. Returns the integer as a list of bytes, where the last byte is < 128. This is used for delta times and meta message payload length. """ if not isinstance(value, Integral) or value < 0: raise ValueError('variable int must be a non-negative integer') bytes = [] while value: bytes.append(value & 0x7f) value >>= 7 if bytes: bytes.reverse() # Set high bit in every byte but the last. for i in range(len(bytes) - 1): bytes[i] |= 0x80 return bytes else: return [0] def decode_variable_int(value): """Decode a list to a variable length integer. Does the opposite of encode_variable_int(value) """ for i in range(len(value) - 1): value[i] &= ~0x80 val = 0 for i in value: val <<= 7 val |= i return val def encode_string(string): return list(bytearray(string.encode(_charset))) def decode_string(data): return bytearray(data).decode(_charset) @contextmanager def meta_charset(tmp_charset): global _charset old = _charset _charset = tmp_charset yield _charset = old def check_int(value, low, high): if not isinstance(value, Integral): raise TypeError('attribute must be an integer') elif not low <= value <= high: raise ValueError(f'attribute must be in range {low}..{high}') def check_str(value): if not isinstance(value, str): raise TypeError('attribute must be a string') class MetaSpec: # The default is to do no checks. def check(self, name, value): pass class MetaSpec_sequence_number(MetaSpec): type_byte = 0x00 attributes = ['number'] defaults = [0] def decode(self, message, data): if len(data) == 0: # Message with length 0 can occur in some files. # (See issues 42 and 93.) message.number = 0 else: message.number = (data[0] << 8) | data[1] def encode(self, message): return [message.number >> 8, message.number & 0xff] def check(self, name, value): check_int(value, 0, 0xffff) class MetaSpec_text(MetaSpec): type_byte = 0x01 attributes = ['text'] defaults = [''] def decode(self, message, data): message.text = decode_string(data) def encode(self, message): return encode_string(message.text) def check(self, name, value): check_str(value) class MetaSpec_copyright(MetaSpec_text): type_byte = 0x02 class MetaSpec_track_name(MetaSpec_text): type_byte = 0x03 attributes = ['name'] defaults = [''] def decode(self, message, data): message.name = decode_string(data) def encode(self, message): return encode_string(message.name) class MetaSpec_instrument_name(MetaSpec_track_name): type_byte = 0x04 class MetaSpec_lyrics(MetaSpec_text): type_byte = 0x05 class MetaSpec_marker(MetaSpec_text): type_byte = 0x06 class MetaSpec_cue_marker(MetaSpec_text): type_byte = 0x07 class MetaSpec_device_name(MetaSpec_track_name): type_byte = 0x09 class MetaSpec_channel_prefix(MetaSpec): type_byte = 0x20 attributes = ['channel'] defaults = [0] def decode(self, message, data): message.channel = data[0] def encode(self, message): return [message.channel] def check(self, name, value): check_int(value, 0, 0xff) class MetaSpec_midi_port(MetaSpec): type_byte = 0x21 attributes = ['port'] defaults = [0] def decode(self, message, data): if len(data) == 0: # Message with length 0 can occur in some files. # (See issues 42 and 93.) message.port = 0 else: message.port = data[0] def encode(self, message): return [message.port] def check(self, name, value): check_int(value, 0, 255) class MetaSpec_end_of_track(MetaSpec): type_byte = 0x2f attributes = [] defaults = [] def decode(self, message, data): pass def encode(self, message): return [] class MetaSpec_set_tempo(MetaSpec): type_byte = 0x51 attributes = ['tempo'] defaults = [500000] def decode(self, message, data): message.tempo = (data[0] << 16) | (data[1] << 8) | (data[2]) def encode(self, message): tempo = message.tempo return [tempo >> 16, tempo >> 8 & 0xff, tempo & 0xff] def check(self, name, value): check_int(value, 0, 0xffffff) class MetaSpec_smpte_offset(MetaSpec): type_byte = 0x54 attributes = ['frame_rate', 'hours', 'minutes', 'seconds', 'frames', 'sub_frames' ] # TODO: What are some good defaults? defaults = [24, 0, 0, 0, 0, 0] def decode(self, message, data): message.frame_rate = _smpte_framerate_decode[(data[0] >> 5)] message.hours = (data[0] & 0b0001_1111) message.minutes = data[1] message.seconds = data[2] message.frames = data[3] message.sub_frames = data[4] def encode(self, message): frame_rate_lookup = _smpte_framerate_encode[message.frame_rate] << 5 return [frame_rate_lookup | message.hours, message.minutes, message.seconds, message.frames, message.sub_frames] def check(self, name, value): if name == 'frame_rate': if value not in _smpte_framerate_encode: valid = ', '.join(sorted(_smpte_framerate_encode.keys())) raise ValueError(f'frame_rate must be one of {valid}') elif name == 'hours': check_int(value, 0, 255) elif name in ['minutes', 'seconds']: check_int(value, 0, 59) elif name == 'frames': check_int(value, 0, 255) elif name == 'sub_frames': check_int(value, 0, 99) class MetaSpec_time_signature(MetaSpec): type_byte = 0x58 # TODO: these need more sensible names. attributes = ['numerator', 'denominator', 'clocks_per_click', 'notated_32nd_notes_per_beat'] defaults = [4, 4, 24, 8] def decode(self, message, data): message.numerator = data[0] message.denominator = 2 ** data[1] message.clocks_per_click = data[2] message.notated_32nd_notes_per_beat = data[3] def encode(self, message): return [message.numerator, int(math.log(message.denominator, 2)), message.clocks_per_click, message.notated_32nd_notes_per_beat, ] def check(self, name, value): if name == 'denominator': # This allows for the ridiculous time signature of # 4/57896044618658097711785492504343953926634... # 992332820282019728792003956564819968 check_int(value, 1, 2 ** 255) encoded = math.log(value, 2) encoded_int = int(encoded) if encoded != encoded_int: raise ValueError('denominator must be a power of 2') else: check_int(value, 0, 255) class MetaSpec_key_signature(MetaSpec): type_byte = 0x59 attributes = ['key'] defaults = ['C'] def decode(self, message, data): key = signed('byte', data[0]) mode = data[1] try: message.key = _key_signature_decode[(key, mode)] except KeyError as ke: if key < 7: msg = ('Could not decode key with {} ' 'flats and mode {}'.format(abs(key), mode)) else: msg = ('Could not decode key with {} ' 'sharps and mode {}'.format(key, mode)) raise KeySignatureError(msg) from ke def encode(self, message): key, mode = _key_signature_encode[message.key] return [unsigned('byte', key), mode] def check(self, name, value): if value not in _key_signature_encode: raise ValueError(f'invalid key {value!r}') class MetaSpec_sequencer_specific(MetaSpec): type_byte = 0x7f attributes = ['data'] defaults = [[]] def decode(self, message, data): message.data = tuple(data) def encode(self, message): return list(message.data) def add_meta_spec(klass): spec = klass() if not hasattr(spec, 'type'): name = klass.__name__.replace('MetaSpec_', '') spec.type = name # This is used by copy(). spec.settable_attributes = set(spec.attributes) | {'time'} _META_SPECS[spec.type_byte] = spec _META_SPECS[spec.type] = spec _META_SPEC_BY_TYPE[spec.type] = spec _META_SPECS = {} _META_SPEC_BY_TYPE = {} def _add_builtin_meta_specs(): for name, spec in globals().items(): if name.startswith('MetaSpec_'): add_meta_spec(spec) _add_builtin_meta_specs() def build_meta_message(meta_type, data, delta=0): # TODO: handle unknown type. try: spec = _META_SPECS[meta_type] except KeyError: return UnknownMetaMessage(meta_type, data) else: msg = MetaMessage(spec.type, time=delta) # This adds attributes to msg: spec.decode(msg, data) return msg class MetaMessage(BaseMessage): is_meta = True def __init__(self, type, skip_checks=False, **kwargs): # TODO: handle unknown type? spec = _META_SPEC_BY_TYPE[type] self_vars = vars(self) self_vars['type'] = type if not skip_checks: for name in kwargs: if name not in spec.settable_attributes: raise ValueError( '{} is not a valid argument for this message type'.format( name)) for name, value in zip(spec.attributes, spec.defaults): self_vars[name] = value self_vars['time'] = 0 for name, value in kwargs.items(): # Using setattr here because we want type and value checks. self._setattr(name, value) def copy(self, **overrides): """Return a copy of the message Attributes will be overridden by the passed keyword arguments. Only message specific attributes can be overridden. The message type can not be changed. """ if not overrides: # Bypass all checks. msg = self.__class__.__new__(self.__class__) vars(msg).update(vars(self)) return msg if 'type' in overrides and overrides['type'] != self.type: raise ValueError('copy must be same message type') attrs = vars(self).copy() attrs.update(overrides) return self.__class__(**attrs) # FrozenMetaMessage overrides __setattr__() but we still need to # set attributes in __init__(). def _setattr(self, name, value): spec = _META_SPEC_BY_TYPE[self.type] self_vars = vars(self) if name in spec.settable_attributes: if name == 'time': check_time(value) else: spec.check(name, value) self_vars[name] = value elif name in self_vars: raise AttributeError(f'{name} attribute is read only') else: raise AttributeError( f'{self.type} message has no attribute {name}') __setattr__ = _setattr def bytes(self): spec = _META_SPEC_BY_TYPE[self.type] data = spec.encode(self) return ([0xff, spec.type_byte] + encode_variable_int(len(data)) + data) @classmethod def from_bytes(cls, msg_bytes): if msg_bytes[0] != 0xff: raise ValueError('bytes does not correspond to a MetaMessage.') scan_end = 2 data = [] flag = True while flag and scan_end < len(msg_bytes): scan_end += 1 length_data = msg_bytes[2:scan_end] length = decode_variable_int(length_data) data = msg_bytes[scan_end:] if length == len(data): flag = False if flag: raise ValueError('Bad data. Cannot be converted to message.') msg = build_meta_message(msg_bytes[1], data) return msg def _get_value_names(self): """Used by BaseMessage.__repr__().""" spec = _META_SPEC_BY_TYPE[self.type] return spec.attributes + ['time'] class UnknownMetaMessage(MetaMessage): def __init__(self, type_byte, data=None, time=0, type='unknown_meta', **kwargs): if data is None: data = () else: data = tuple(data) vars(self).update({ 'type': type, 'type_byte': type_byte, 'data': data, 'time': time}) def __repr__(self): fmt = 'UnknownMetaMessage(type_byte={}, data={}, time={})' return fmt.format(self.type_byte, self.data, self.time) def __setattr__(self, name, value): # This doesn't do any checking. # It probably should. vars(self)[name] = value def bytes(self): length = encode_variable_int(len(self.data)) return ([0xff, self.type_byte] + length + list(self.data)) mido-1.3.3/mido/midifiles/midifiles.py0000644000175100001770000003507514706731605020512 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2016 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT """ MIDI file reading and playback. References: http://home.roadrunner.com/~jgglatt/ http://home.roadrunner.com/~jgglatt/tech/miditech.htm http://home.roadrunner.com/~jgglatt/tech/midifile.htm http://www.sonicspot.com/guide/midifiles.html http://www.ccarh.org/courses/253/assignment/midifile/ https://code.google.com/p/binasc/wiki/mainpage http://stackoverflow.com/questions/2984608/midi-delta-time http://www.recordingblogs.com/sa/tabid/82/EntryId/44/MIDI-Part-XIII-Delta-time-a http://www.sonicspot.com/guide/midifiles.html """ import string import struct import time from numbers import Integral from ..messages import SPEC_BY_STATUS, Message from .meta import MetaMessage, build_meta_message, encode_variable_int, meta_charset from .tracks import MidiTrack, fix_end_of_track, merge_tracks from .units import tick2second # The default tempo is 120 BPM. # (500000 microseconds per beat (quarter note).) DEFAULT_TEMPO = 500000 DEFAULT_TICKS_PER_BEAT = 480 # Maximum message length to attempt to read. MAX_MESSAGE_LENGTH = 1000000 def print_byte(byte, pos=0): char = chr(byte) if char.isspace() or char not in string.printable: char = '.' print(f' {pos:06x}: {byte:02x} {char}') # noqa: T201 class DebugFileWrapper: def __init__(self, file): self.file = file def read(self, size): data = self.file.read(size) for byte in data: print_byte(byte, self.file.tell()) return data def tell(self): return self.file.tell() def read_byte(self): byte = self.read(1) if byte == b'': raise EOFError else: return ord(byte) def read_bytes(infile, size): if size > MAX_MESSAGE_LENGTH: raise OSError('Message length {} exceeds maximum length {}'.format( size, MAX_MESSAGE_LENGTH)) return [read_byte(infile) for _ in range(size)] def _dbg(text=''): print(text) # noqa: T201 # We can't use the chunk module for two reasons: # # 1. we may have mixed big and little endian chunk sizes. (RIFF is # little endian while MTrk is big endian.) # # 2. the chunk module assumes that chunks are padded to the nearest # multiple of 2. This is not true of MIDI files. def read_chunk_header(infile): header = infile.read(8) if len(header) < 8: raise EOFError # TODO: check for b'RIFF' and switch endian? return struct.unpack('>4sL', header) def read_file_header(infile): name, size = read_chunk_header(infile) if name != b'MThd': raise OSError('MThd not found. Probably not a MIDI file') else: data = infile.read(size) if len(data) < 6: raise EOFError return struct.unpack('>hhh', data[:6]) def read_message(infile, status_byte, peek_data, delta, clip=False): try: spec = SPEC_BY_STATUS[status_byte] except LookupError as le: raise OSError(f'undefined status byte 0x{status_byte:02x}') from le # Subtract 1 for status byte. size = spec['length'] - 1 - len(peek_data) data_bytes = peek_data + read_bytes(infile, size) if clip: data_bytes = [byte if byte < 127 else 127 for byte in data_bytes] else: for byte in data_bytes: if byte > 127: raise OSError('data byte must be in range 0..127') return Message.from_bytes([status_byte] + data_bytes, time=delta) def read_sysex(infile, delta, clip=False): length = read_variable_int(infile) data = read_bytes(infile, length) # Strip start and end bytes. # TODO: is this necessary? if data and data[0] == 0xf0: data = data[1:] if data and data[-1] == 0xf7: data = data[:-1] if clip: data = [byte if byte < 127 else 127 for byte in data] return Message('sysex', data=data, time=delta) def read_variable_int(infile): delta = 0 while True: byte = read_byte(infile) delta = (delta << 7) | (byte & 0x7f) if byte < 0x80: return delta def read_meta_message(infile, delta): meta_type = read_byte(infile) length = read_variable_int(infile) data = read_bytes(infile, length) return build_meta_message(meta_type, data, delta) def read_track(infile, debug=False, clip=False): track = MidiTrack() name, size = read_chunk_header(infile) if name != b'MTrk': raise OSError('no MTrk header at start of track') if debug: _dbg(f'-> size={size}') _dbg() start = infile.tell() last_status = None while True: # End of track reached. if infile.tell() - start == size: break if debug: _dbg('Message:') delta = read_variable_int(infile) if debug: _dbg(f'-> delta={delta}') status_byte = read_byte(infile) if status_byte < 0x80: if last_status is None: raise OSError('running status without last_status') peek_data = [status_byte] status_byte = last_status else: if status_byte != 0xff: # Meta messages don't set running status. last_status = status_byte peek_data = [] if status_byte == 0xff: msg = read_meta_message(infile, delta) elif status_byte in [0xf0, 0xf7]: # TODO: I'm not quite clear on the difference between # f0 and f7 events. msg = read_sysex(infile, delta, clip) else: msg = read_message(infile, status_byte, peek_data, delta, clip) track.append(msg) if debug: _dbg(f'-> {msg!r}') _dbg() return track def write_chunk(outfile, name, data): """Write an IFF chunk to the file. `name` must be a bytestring.""" outfile.write(name) outfile.write(struct.pack('>L', len(data))) outfile.write(data) def write_track(outfile, track): data = bytearray() running_status_byte = None for msg in fix_end_of_track(track): if not isinstance(msg.time, Integral): raise ValueError('message time must be int in MIDI file') if msg.time < 0: raise ValueError('message time must be non-negative in MIDI file') if msg.is_realtime: raise ValueError('realtime messages are not allowed in MIDI files') data.extend(encode_variable_int(msg.time)) if msg.is_meta: data.extend(msg.bytes()) running_status_byte = None elif msg.type == 'sysex': data.append(0xf0) # length (+ 1 for end byte (0xf7)) data.extend(encode_variable_int(len(msg.data) + 1)) data.extend(msg.data) data.append(0xf7) running_status_byte = None else: msg_bytes = msg.bytes() status_byte = msg_bytes[0] if status_byte == running_status_byte: data.extend(msg_bytes[1:]) else: data.extend(msg_bytes) if status_byte < 0xf0: running_status_byte = status_byte else: running_status_byte = None write_chunk(outfile, b'MTrk', data) def get_seconds_per_tick(tempo, ticks_per_beat): # Tempo is given in microseconds per beat (default 500000). # At this tempo there are (500000 / 1000000) == 0.5 seconds # per beat. At the default resolution of 480 ticks per beat # this is: # # (500000 / 1000000) / 480 == 0.5 / 480 == 0.0010417 # return (tempo / 1000000.0) / ticks_per_beat class MidiFile: def __init__(self, filename=None, file=None, type=1, ticks_per_beat=DEFAULT_TICKS_PER_BEAT, charset='latin1', debug=False, clip=False, tracks=None ): self.filename = filename self.type = type self.ticks_per_beat = ticks_per_beat self.charset = charset self.debug = debug self.clip = clip self.tracks = [] self._merged_track = None if type not in range(3): raise ValueError( f'invalid format {format} (must be 0, 1 or 2)') if tracks is not None: self.tracks = tracks elif file is not None: self._load(file) elif self.filename is not None: with open(filename, 'rb') as file: self._load(file) @property def merged_track(self): # The tracks of type 2 files are not in sync, so they can # not be played back like this. if self.type == 2: raise TypeError("can't merge tracks in type 2 (asynchronous) file") if self._merged_track is None: self._merged_track = merge_tracks(self.tracks, skip_checks=True) return self._merged_track @merged_track.deleter def merged_track(self): self._merged_track = None def add_track(self, name=None): """Add a new track to the file. This will create a new MidiTrack object and append it to the track list. """ track = MidiTrack() if name is not None: track.name = name self.tracks.append(track) del self.merged_track # uncache merged track return track def _load(self, infile): if self.debug: infile = DebugFileWrapper(infile) with meta_charset(self.charset): if self.debug: _dbg('Header:') (self.type, num_tracks, self.ticks_per_beat) = read_file_header(infile) if self.debug: _dbg('-> type={}, tracks={}, ticks_per_beat={}'.format( self.type, num_tracks, self.ticks_per_beat)) _dbg() for i in range(num_tracks): if self.debug: _dbg(f'Track {i}:') self.tracks.append(read_track(infile, debug=self.debug, clip=self.clip)) # TODO: used to ignore EOFError. I hope things still work. @property def length(self): """Playback time in seconds. This will be computed by going through every message in every track and adding up delta times. """ if self.type == 2: raise ValueError('impossible to compute length' ' for type 2 (asynchronous) file') return sum(msg.time for msg in self) def __iter__(self): tempo = DEFAULT_TEMPO for msg in self.merged_track: # Convert message time from absolute time # in ticks to relative time in seconds. if msg.time > 0: delta = tick2second(msg.time, self.ticks_per_beat, tempo) else: delta = 0 yield msg.copy(skip_checks=True, time=delta) if msg.type == 'set_tempo': tempo = msg.tempo def play(self, meta_messages=False, now=time.time): """Play back all tracks. The generator will sleep between each message by default. Messages are yielded with correct timing. The time attribute is set to the number of seconds slept since the previous message. By default you will only get normal MIDI messages. Pass meta_messages=True if you also want meta messages. You will receive copies of the original messages, so you can safely modify them without ruining the tracks. By default the system clock is used for the timing of yielded MIDI events. To use a different clock (e.g. to synchronize to an audio stream), pass now=time_fn where time_fn is a zero argument function that yields the current time in seconds. """ start_time = now() input_time = 0.0 for msg in self: input_time += msg.time playback_time = now() - start_time duration_to_next_event = input_time - playback_time if duration_to_next_event > 0.0: time.sleep(duration_to_next_event) if isinstance(msg, MetaMessage) and not meta_messages: continue else: yield msg def save(self, filename=None, file=None): """Save to a file. If file is passed the data will be saved to that file. This is typically an in-memory file or and already open file like sys.stdout. If filename is passed the data will be saved to that file. Raises ValueError if both file and filename are None, or if a type 0 file has != one track. """ if self.type == 0 and len(self.tracks) != 1: raise ValueError('type 0 file must have exactly 1 track') if file is not None: self._save(file) elif filename is not None: with open(filename, 'wb') as file: self._save(file) else: raise ValueError('requires filename or file') def _save(self, outfile): with meta_charset(self.charset): header = struct.pack('>hhh', self.type, len(self.tracks), self.ticks_per_beat) write_chunk(outfile, b'MThd', header) for track in self.tracks: write_track(outfile, track) def print_tracks(self, meta_only=False): """Prints out all messages in a .midi file. May take argument meta_only to show only meta messages. Use: print_tracks() -> will print all messages print_tracks(meta_only=True) -> will print only MetaMessages """ for i, track in enumerate(self.tracks): print(f'=== Track {i}') # noqa: T201 for msg in track: if isinstance(msg, MetaMessage) or not meta_only: print(f'{msg!r}') # noqa: T201 def __repr__(self): if self.tracks: tracks_str = ',\n'.join(repr(track) for track in self.tracks) tracks_str = ' ' + tracks_str.replace('\n', '\n ') tracks_str = f', tracks=[\n{tracks_str}\n]' else: tracks_str = '' return '{}(type={}, ticks_per_beat={}{})'.format( self.__class__.__name__, self.type, self.ticks_per_beat, tracks_str, ) # The context manager has no purpose but is kept around since it was # used in examples in the past. def __enter__(self): return self def __exit__(self, type, value, traceback): return False mido-1.3.3/mido/midifiles/tracks.py0000644000175100001770000000752414706731605020032 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2016 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT from .meta import MetaMessage class MidiTrack(list): @property def name(self): """Name of the track. This will return the name from the first track_name meta message in the track, or '' if there is no such message. Setting this property will update the name field of the first track_name message in the track. If no such message is found, one will be added to the beginning of the track with a delta time of 0.""" for message in self: if message.type == 'track_name': return message.name else: return '' @name.setter def name(self, name): # Find the first track_name message and modify it. for message in self: if message.type == 'track_name': message.name = name return else: # No track name found, add one. self.insert(0, MetaMessage('track_name', name=name, time=0)) def copy(self): return self.__class__(self) def __getitem__(self, index_or_slice): # Retrieve item from the MidiTrack lst = list.__getitem__(self, index_or_slice) if isinstance(index_or_slice, int): # If an index was provided, return the list element return lst else: # Otherwise, construct a MidiTrack to return. # TODO: this make a copy of the list. Is there a better way? return self.__class__(lst) def __add__(self, other): return self.__class__(list.__add__(self, other)) def __mul__(self, other): return self.__class__(list.__mul__(self, other)) def __repr__(self): if len(self) == 0: messages = '' elif len(self) == 1: messages = f'[{self[0]}]' else: messages = '[\n {}]'.format(',\n '.join(repr(m) for m in self)) return f'{self.__class__.__name__}({messages})' def _to_abstime(messages, skip_checks=False): """Convert messages to absolute time.""" now = 0 for msg in messages: now += msg.time yield msg.copy(skip_checks=skip_checks, time=now) def _to_reltime(messages, skip_checks=False): """Convert messages to relative time.""" now = 0 for msg in messages: delta = msg.time - now yield msg.copy(skip_checks=skip_checks, time=delta) now = msg.time def fix_end_of_track(messages, skip_checks=False): """Remove all end_of_track messages and add one at the end. This is used by merge_tracks() and MidiFile.save().""" # Accumulated delta time from removed end of track messages. # This is added to the next message. accum = 0 for msg in messages: if msg.type == 'end_of_track': accum += msg.time else: if accum: delta = accum + msg.time yield msg.copy(skip_checks=skip_checks, time=delta) accum = 0 else: yield msg yield MetaMessage('end_of_track', time=accum) def merge_tracks(tracks, skip_checks=False): """Returns a MidiTrack object with all messages from all tracks. The messages are returned in playback order with delta times as if they were all in one track. Pass skip_checks=True to skip validation of messages before merging. This should ONLY be used when the messages in tracks have already been validated by mido.checks. """ messages = [] for track in tracks: messages.extend(_to_abstime(track, skip_checks=skip_checks)) messages.sort(key=lambda msg: msg.time) return MidiTrack( fix_end_of_track( _to_reltime(messages, skip_checks=skip_checks), skip_checks=skip_checks, ) ) mido-1.3.3/mido/midifiles/units.py0000644000175100001770000000324514706731605017701 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2017 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT def tick2second(tick, ticks_per_beat, tempo): """Convert absolute time in ticks to seconds. Returns absolute time in seconds for a chosen MIDI file time resolution (ticks/pulses per quarter note, also called PPQN) and tempo (microseconds per quarter note). """ scale = tempo * 1e-6 / ticks_per_beat return tick * scale def second2tick(second, ticks_per_beat, tempo): """Convert absolute time in seconds to ticks. Returns absolute time in ticks for a chosen MIDI file time resolution (ticks/pulses per quarter note, also called PPQN) and tempo (microseconds per quarter note). Normal rounding applies. """ scale = tempo * 1e-6 / ticks_per_beat return int(round(second / scale)) def bpm2tempo(bpm, time_signature=(4, 4)): """Convert BPM (beats per minute) to MIDI file tempo (microseconds per quarter note). Depending on the chosen time signature a bar contains a different number of beats. These beats are multiples/fractions of a quarter note, thus the returned BPM depend on the time signature. Normal rounding applies. """ return int(round(60 * 1e6 / bpm * time_signature[1] / 4.)) def tempo2bpm(tempo, time_signature=(4, 4)): """Convert MIDI file tempo (microseconds per quarter note) to BPM (beats per minute). Depending on the chosen time signature a bar contains a different number of beats. The beats are multiples/fractions of a quarter note, thus the returned tempo depends on the time signature denominator. """ return 60 * 1e6 / tempo * time_signature[1] / 4. mido-1.3.3/mido/parser.py0000644000175100001770000000534614706731605016072 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT """ MIDI Parser There is no need to use this module directly. All you need is available in the top level module. """ from collections import deque from .messages import Message from .tokenizer import Tokenizer class Parser: """ MIDI byte stream parser Parses a stream of MIDI bytes and produces messages. Data can be put into the parser in the form of integers, byte arrays or byte strings. """ def __init__(self, data=None): # For historical reasons self.messages is public and must be a # deque(). (It is referenced directly inside ports.) self.messages = deque() self._tok = Tokenizer() if data: self.feed(data) def _decode(self): for midi_bytes in self._tok: self.messages.append(Message.from_bytes(midi_bytes)) def feed(self, data): """Feed MIDI data to the parser. Accepts any object that produces a sequence of integers in range 0..255, such as: [0, 1, 2] (0, 1, 2) [for i in range(256)] (for i in range(256)] bytearray() """ self._tok.feed(data) self._decode() def feed_byte(self, byte): """Feed one MIDI byte into the parser. The byte must be an integer in range 0..255. """ self._tok.feed_byte(byte) self._decode() def get_message(self): """Get the first parsed message. Returns None if there is no message yet. If you don't want to deal with None, you can use pending() to see how many messages you can get before you get None, or just iterate over the parser. """ for msg in self: return msg else: return None def pending(self): """Return the number of pending messages.""" return len(self.messages) __len__ = pending def __iter__(self): """Yield messages that have been parsed so far.""" while len(self.messages) > 0: yield self.messages.popleft() def parse_all(data): """Parse MIDI data and return a list of all messages found. This is typically used to parse a little bit of data with a few messages in it. It's best to use a Parser object for larger amounts of data. Also, tt's often easier to use parse() if you know there is only one message in the data. """ return list(Parser(data)) def parse(data): """ Parse MIDI data and return the first message found. Data after the first message is ignored. Use parse_all() to parse more than one message. """ return Parser(data).get_message() mido-1.3.3/mido/ports.py0000644000175100001770000002711014706731605015736 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT """ Useful tools for working with ports """ import random import threading import time from .messages import Message from .parser import Parser # How many seconds to sleep before polling again. _DEFAULT_SLEEP_TIME = 0.001 _sleep_time = _DEFAULT_SLEEP_TIME # TODO: document this more. def sleep(): """Sleep for N seconds. This is used in ports when polling and waiting for messages. N can be set with set_sleep_time().""" time.sleep(_sleep_time) def set_sleep_time(seconds=_DEFAULT_SLEEP_TIME): """Set the number of seconds sleep() will sleep.""" global _sleep_time _sleep_time = seconds def get_sleep_time(): """Get number of seconds sleep() will sleep.""" return _sleep_time def reset_messages(): """Yield "All Notes Off" and "Reset All Controllers" for all channels""" ALL_NOTES_OFF = 123 RESET_ALL_CONTROLLERS = 121 for channel in range(16): for control in [ALL_NOTES_OFF, RESET_ALL_CONTROLLERS]: yield Message('control_change', channel=channel, control=control) def panic_messages(): """Yield "All Sounds Off" for all channels. This will mute all sounding notes regardless of envelopes. Useful when notes are hanging and nothing else helps. """ ALL_SOUNDS_OFF = 120 for channel in range(16): yield Message('control_change', channel=channel, control=ALL_SOUNDS_OFF) class DummyLock: def __enter__(self): return self def __exit__(self, *_): return False class BasePort: """ Abstract base class for Input and Output ports. """ is_input = False is_output = False _locking = True def __init__(self, name=None, **kwargs): if hasattr(self, 'closed'): # __init__() called twice (from BaseInput and BaseOutput). # This stops _open() from being called twice. return self.name = name if self._locking: self._lock = threading.RLock() else: self._lock = DummyLock() self.closed = True self._open(**kwargs) self.closed = False def _open(self, **kwargs): pass def _close(self): pass def close(self): """Close the port. If the port is already closed, nothing will happen. The port is automatically closed when the object goes out of scope or is garbage collected. """ with self._lock: if not self.closed: if hasattr(self, 'autoreset') and self.autoreset: try: self.reset() except OSError: pass self._close() self.closed = True def __del__(self): self.close() def __enter__(self): return self def __exit__(self, type, value, traceback): self.close() return False def __repr__(self): if self.closed: state = 'closed' else: state = 'open' capabilities = self.is_input, self.is_output port_type = {(True, False): 'input', (False, True): 'output', (True, True): 'I/O port', (False, False): 'mute port', }[capabilities] name = self.name or '' try: device_type = self._device_type except AttributeError: device_type = self.__class__.__name__ return '<{} {} {!r} ({})>'.format( state, port_type, name, device_type) class BaseInput(BasePort): """Base class for input port. Subclass and override _receive() to create a new input port type. (See portmidi.py for an example of how to do this.) """ is_input = True def __init__(self, name='', **kwargs): """Create an input port. name is the port name, as returned by input_names(). If name is not passed, the default input is used instead. """ BasePort.__init__(self, name, **kwargs) self._parser = Parser() self._messages = self._parser.messages # Shortcut. def _check_callback(self): if hasattr(self, 'callback') and self.callback is not None: raise ValueError('a callback is set for this port') def _receive(self, block=True): pass def iter_pending(self): """Iterate through pending messages.""" while True: msg = self.poll() if msg is None: return else: yield msg def receive(self, block=True): """Return the next message. This will block until a message arrives. If you pass block=False it will not block and instead return None if there is no available message. If the port is closed and there are no pending messages IOError will be raised. If the port closes while waiting inside receive(), IOError will be raised. TODO: this seems a bit inconsistent. Should different errors be raised? What's most useful here? """ if not self.is_input: raise ValueError('Not an input port') self._check_callback() # If there is a message pending, return it right away. with self._lock: if self._messages: return self._messages.popleft() if self.closed: if block: raise ValueError('receive() called on closed port') else: return None while True: with self._lock: msg = self._receive(block=block) if msg: return msg if self._messages: return self._messages.popleft() elif not block: return None elif self.closed: raise OSError('port closed during receive()') sleep() def poll(self): """Receive the next pending message or None This is the same as calling `receive(block=False)`.""" return self.receive(block=False) def __iter__(self): """Iterate through messages until the port closes.""" # This could have simply called receive() in a loop, but that # could result in a "port closed during receive()" error which # is hard to catch here. self._check_callback() while True: try: yield self.receive() except OSError: if self.closed: # The port closed before or inside receive(). # (This makes the assumption that this is the reason, # at the risk of masking other errors.) return else: raise class BaseOutput(BasePort): """ Base class for output port. Subclass and override _send() to create a new port type. (See portmidi.py for how to do this.) """ is_output = True def __init__(self, name='', autoreset=False, **kwargs): """Create an output port name is the port name, as returned by output_names(). If name is not passed, the default output is used instead. """ BasePort.__init__(self, name, **kwargs) self.autoreset = autoreset def _send(self, msg): pass def send(self, msg): """Send a message on the port. A copy of the message will be sent, so you can safely modify the original message without any unexpected consequences. """ if not self.is_output: raise ValueError('Not an output port') elif not isinstance(msg, Message): raise TypeError('argument to send() must be a Message') elif self.closed: raise ValueError('send() called on closed port') with self._lock: self._send(msg.copy()) def reset(self): """Send "All Notes Off" and "Reset All Controllers" on all channels""" if self.closed: return for msg in reset_messages(): self.send(msg) def panic(self): """Send "All Sounds Off" on all channels. This will mute all sounding notes regardless of envelopes. Useful when notes are hanging and nothing else helps. """ if self.closed: return for msg in panic_messages(): self.send(msg) class BaseIOPort(BaseInput, BaseOutput): def __init__(self, name='', **kwargs): """Create an IO port. name is the port name, as returned by ioport_names(). """ BaseInput.__init__(self, name, **kwargs) BaseOutput.__init__(self, name, **kwargs) class IOPort(BaseIOPort): """Input / output port. This is a convenient wrapper around an input port and an output port which provides the functionality of both. Every method call is forwarded to the appropriate port. """ _locking = False def __init__(self, input, output): self.input = input self.output = output # We use str() here in case name is None. self.name = f'{str(input.name)} + {str(output.name)}' self._messages = self.input._messages self.closed = False self._lock = DummyLock() def _close(self): self.input.close() self.output.close() def _send(self, message): self.output.send(message) def _receive(self, block=True): return self.input.receive(block=block) class EchoPort(BaseIOPort): def _send(self, message): self._messages.append(message) __iter__ = BaseIOPort.iter_pending class MultiPort(BaseIOPort): def __init__(self, ports, yield_ports=False): BaseIOPort.__init__(self, 'multi') self.ports = list(ports) self.yield_ports = yield_ports def _send(self, message): for port in self.ports: if not port.closed: # TODO: what if a SocketPort connection closes in-between here? port.send(message) def _receive(self, block=True): self._messages.extend(multi_receive(self.ports, yield_ports=self.yield_ports, block=block)) def multi_receive(ports, yield_ports=False, block=True): """Receive messages from multiple ports. Generates messages from ever input port. The ports are polled in random order for fairness, and all messages from each port are yielded before moving on to the next port. If yield_ports=True, (port, message) is yielded instead of just the message. If block=False only pending messages will be yielded. """ ports = list(ports) while True: # Make a shuffled copy of the port list. random.shuffle(ports) for port in ports: if not port.closed: for message in port.iter_pending(): if yield_ports: yield port, message else: yield message if block: sleep() else: break def multi_iter_pending(ports, yield_ports=False): """Iterate through all pending messages in ports. This is the same as calling multi_receive(ports, block=False). The function is kept around for backwards compatability. """ return multi_receive(ports, yield_ports=yield_ports, block=False) def multi_send(ports, msg): """Send message on all ports.""" for port in ports: port.send(msg) mido-1.3.3/mido/scripts/0000755000175100001770000000000014706731615015704 5ustar runnerdocker00000000000000mido-1.3.3/mido/scripts/__init__.py0000644000175100001770000000015314706731605020013 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2011 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT mido-1.3.3/mido/scripts/mido_connect.py0000644000175100001770000000204214706731605020714 0ustar runnerdocker00000000000000#!/usr/bin/env python3 # SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT """ Forward all messages from one or more ports to server. """ import argparse import mido def parse_args(): parser = argparse.ArgumentParser(description=__doc__) arg = parser.add_argument arg('address', metavar='ADDRESS', help='host:port to connect to') arg('ports', metavar='PORT', nargs='+', help='input ports to listen to') return parser.parse_args() def main(): args = parse_args() try: hostname, port = mido.sockets.parse_address(args.address) ports = [mido.open_input(name) for name in args.ports] with mido.sockets.connect(hostname, port) as server_port: print('Connected.') for message in mido.ports.multi_receive(ports): print(f'Sending {message}') server_port.send(message) except KeyboardInterrupt: pass if __name__ == '__main__': main() mido-1.3.3/mido/scripts/mido_play.py0000644000175100001770000000437514706731605020243 0ustar runnerdocker00000000000000#!/usr/bin/env python3 # SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT """ Play MIDI file on output port. Example: mido-play some_file.mid Todo: - add option for printing messages """ import argparse import sys import mido from mido import Message, MidiFile, tempo2bpm def parse_args(): parser = argparse.ArgumentParser(description=__doc__) arg = parser.add_argument arg('-o', '--output-port', help='Mido port to send output to') arg('-m', '--print-messages', dest='print_messages', action='store_true', default=False, help='Print messages as they are played back') arg('-q', '--quiet', dest='quiet', action='store_true', default=False, help='print nothing') arg('files', metavar='FILE', nargs='+', help='MIDI file to play') return parser.parse_args() def play_file(output, filename, print_messages): midi_file = MidiFile(filename) print(f'Playing {midi_file.filename}.') length = midi_file.length print('Song length: {} minutes, {} seconds.'.format( int(length / 60), int(length % 60))) print('Tracks:') for i, track in enumerate(midi_file.tracks): print(f' {i:2d}: {track.name.strip()!r}') for message in midi_file.play(meta_messages=True): if print_messages: sys.stdout.write(repr(message) + '\n') sys.stdout.flush() if isinstance(message, Message): output.send(message) elif message.type == 'set_tempo': print('Tempo changed to {:.1f} BPM.'.format( tempo2bpm(message.tempo))) print() def main(): args = parse_args() if args.quiet: global print def print(*args): pass try: with mido.open_output(args.output_port) as output: print(f'Using output {output.name!r}.') output.reset() try: for filename in args.files: play_file(output, filename, args.print_messages) finally: print() output.reset() except KeyboardInterrupt: pass if __name__ == '__main__': main() mido-1.3.3/mido/scripts/mido_ports.py0000644000175100001770000000161314706731605020435 0ustar runnerdocker00000000000000#!/usr/bin/env python3 # SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT """ List available PortMidi ports. """ import os import mido def print_ports(heading, port_names): print(heading) for name in port_names: print(f" '{name}'") print() def main(): print() print_ports('Available input Ports:', mido.get_input_names()) print_ports('Available output Ports:', mido.get_output_names()) for name in ['MIDO_DEFAULT_INPUT', 'MIDO_DEFAULT_OUTPUT', 'MIDO_DEFAULT_IOPORT', 'MIDO_BACKEND']: try: value = os.environ[name] print(f'{name}={value!r}') except LookupError: print(f'{name} not set.') print() print(f'Using backend {mido.backend.name}.') print() if __name__ == '__main__': main() mido-1.3.3/mido/scripts/mido_serve.py0000644000175100001770000000213114706731605020406 0ustar runnerdocker00000000000000#!/usr/bin/env python3 # SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT """ Serve one or more output ports. Every message received on any of the connected sockets will be sent to every output port. """ import argparse import mido from mido import sockets from mido.ports import MultiPort def parse_args(): parser = argparse.ArgumentParser(description=__doc__) arg = parser.add_argument arg('address', metavar='ADDRESS', help='host:port to serve on') arg('ports', metavar='PORT', nargs='+', help='output port to serve') return parser.parse_args() def main(): args = parse_args() try: out = MultiPort([mido.open_output(name) for name in args.ports]) (hostname, port) = sockets.parse_address(args.address) with sockets.PortServer(hostname, port) as server: for message in server: print(f'Received {message}') out.send(message) except KeyboardInterrupt: pass if __name__ == '__main__': main() mido-1.3.3/mido/sockets.py0000644000175100001770000001065714706731605016252 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT """ MIDI over TCP/IP. """ import select import socket from .parser import Parser from .ports import BaseIOPort, MultiPort def _is_readable(socket): """Return True if there is data to be read on the socket.""" timeout = 0 (rlist, wlist, elist) = select.select( [socket.fileno()], [], [], timeout) return bool(rlist) class PortServer(MultiPort): # TODO: queue size. def __init__(self, host, portno, backlog=1): MultiPort.__init__(self, format_address(host, portno)) self.ports = [] self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) self._socket.setblocking(True) self._socket.bind((host, portno)) self._socket.listen(backlog) def _get_device_type(self): return 'server' def _close(self): # Close all connections. for port in self.ports: port.close() self._socket.close() def _update_ports(self): """Remove closed port ports.""" self.ports = [port for port in self.ports if not port.closed] def accept(self, block=True): """ Accept a connection from a client. Will block until there is a new connection, and then return a SocketPort object. If block=False, None will be returned if there is no new connection waiting. """ if not block and not _is_readable(self._socket): return None self._update_ports() conn, (host, port) = self._socket.accept() return SocketPort(host, port, conn=conn) def _send(self, message): self._update_ports() return MultiPort._send(self, message) def _receive(self, block=True): port = self.accept(block=False) if port: self.ports.append(port) self._update_ports() return MultiPort._receive(self) class SocketPort(BaseIOPort): def __init__(self, host, portno, conn=None): BaseIOPort.__init__(self, name=format_address(host, portno)) self.closed = False self._parser = Parser() self._messages = self._parser.messages if conn is None: self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self._socket.setblocking(True) self._socket.connect((host, portno)) else: self._socket = conn kwargs = {'buffering': 0} self._rfile = self._socket.makefile('rb', **kwargs) self._wfile = self._socket.makefile('wb', **kwargs) def _get_device_type(self): return 'socket' def _receive(self, block=True): while _is_readable(self._socket): try: byte = self._rfile.read(1) except OSError as err: raise OSError(err.args[1]) from err if len(byte) == 0: # The other end has disconnected. self.close() break else: self._parser.feed_byte(ord(byte)) def _send(self, message): try: self._wfile.write(message.bin()) self._wfile.flush() except OSError as err: if err.errno == 32: # Broken pipe. The other end has disconnected. self.close() raise OSError(err.args[1]) from err def _close(self): self._socket.close() def connect(host, portno): """Connect to a socket port server. The return value is a SocketPort object connected to another SocketPort object at the server end. Messages can be sent either way. """ return SocketPort(host, portno) def parse_address(address): """Parse and address on the format host:port. Returns a tuple (host, port). Raises ValueError if format is invalid or port is not an integer or out of range. """ words = address.split(':') if len(words) != 2: raise ValueError('address must contain exactly one colon') host, port = words try: port = int(port) except ValueError as ve: raise ValueError('port number must be an integer') from ve # Note: port 0 is not allowed. if not 0 < port < (2**16): raise ValueError('port number out of range') return (host, port) def format_address(host, portno): return f'{host}{portno:d}' mido-1.3.3/mido/syx.py0000644000175100001770000000310414706731605015407 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2014 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT """ Read and write SYX file format """ import re from .parser import Parser def read_syx_file(filename): """Read sysex messages from SYX file. Returns a list of sysex messages. This handles both the text (hexadecimal) and binary formats. Messages other than sysex will be ignored. Raises ValueError if file is plain text and byte is not a 2-digit hex number. """ with open(filename, 'rb') as infile: data = infile.read() if len(data) == 0: # Empty file. return [] parser = Parser() if data[0] == 240: # Binary format. parser.feed(data) else: text = data.decode('latin1') data = bytearray.fromhex(re.sub(r'\s', ' ', text)) parser.feed(data) return [msg for msg in parser if msg.type == 'sysex'] def write_syx_file(filename, messages, plaintext=False): """Write sysex messages to a SYX file. Messages other than sysex will be skipped. By default this will write the binary format. Pass ``plaintext=True`` to write the plain text format (hex encoded ASCII text). """ messages = [m for m in messages if m.type == 'sysex'] if plaintext: with open(filename, 'w') as outfile: for message in messages: outfile.write(message.hex()) outfile.write('\n') else: with open(filename, 'wb') as outfile: for message in messages: outfile.write(message.bin()) mido-1.3.3/mido/tokenizer.py0000644000175100001770000000550614706731605016606 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2017 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT from collections import deque from numbers import Integral from .messages.specs import SPEC_BY_STATUS, SYSEX_END, SYSEX_START class Tokenizer: """ Splits a MIDI byte stream into messages. """ def __init__(self, data=None): """Create a new decoder.""" self._status = 0 self._bytes = [] self._messages = deque() self._datalen = 0 if data is not None: self.feed(data) def _feed_status_byte(self, status): if status == SYSEX_END: if self._status == SYSEX_START: self._bytes.append(SYSEX_END) self._messages.append(self._bytes) self._status = 0 elif 0xf8 <= status <= 0xff: if self._status != SYSEX_START: # Realtime messages are only allowed inside sysex # messages. Reset parser. self._status = 0 if status in SPEC_BY_STATUS: self._messages.append([status]) elif status in SPEC_BY_STATUS: # New message. spec = SPEC_BY_STATUS[status] if spec['length'] == 1: self._messages.append([status]) self._status = 0 else: self._status = status self._bytes = [status] self._len = spec['length'] else: # Undefined message. Reset parser. # (Undefined realtime messages are handled above.) # self._status = 0 pass def _feed_data_byte(self, byte): if self._status: self._bytes.append(byte) if len(self._bytes) == self._len: # Complete message. self._messages.append(self._bytes) self._status = 0 else: # Ignore stray data byte. pass def feed_byte(self, byte): """Feed MIDI byte to the decoder. Takes an int in range [0..255]. """ if not isinstance(byte, Integral): raise TypeError('message byte must be integer') if 0 <= byte <= 255: if byte <= 127: return self._feed_data_byte(byte) else: return self._feed_status_byte(byte) else: raise ValueError(f'invalid byte value {byte!r}') def feed(self, data): """Feed MIDI bytes to the decoder. Takes an iterable of ints in in range [0..255]. """ for byte in data: self.feed_byte(byte) def __len__(self): return len(self._messages) def __iter__(self): """Yield messages that have been parsed so far.""" while len(self._messages): yield self._messages.popleft() mido-1.3.3/mido/version.py0000644000175100001770000000100414706731605016246 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2016 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT import packaging.version try: # Python 3.8+ import importlib.metadata as importlib_metadata except ImportError: # Python 3.7 and lower import importlib_metadata __version__ = "0.0.0.dev0" try: __version__ = importlib_metadata.version("mido") except importlib_metadata.PackageNotFoundError: # Package is not installed pass version_info = packaging.version.Version(__version__) mido-1.3.3/mido.egg-info/0000755000175100001770000000000014706731615015707 5ustar runnerdocker00000000000000mido-1.3.3/mido.egg-info/PKG-INFO0000644000175100001770000001227014706731615017006 0ustar runnerdocker00000000000000Metadata-Version: 2.1 Name: mido Version: 1.3.3 Summary: MIDI Objects for Python Author-email: Ole Martin Bjorndalen Maintainer-email: Radovan Bast , Raphaël Doursenaud License: MIT Project-URL: documentation, https://mido.readthedocs.io Project-URL: source, https://github.com/mido/mido Keywords: python,midi,midifile Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Natural Language :: English Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Topic :: Multimedia :: Sound/Audio :: MIDI Classifier: Topic :: Software Development Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Utilities Requires-Python: ~=3.7 Description-Content-Type: text/x-rst Provides-Extra: build-docs Provides-Extra: check-manifest Provides-Extra: lint-code Provides-Extra: lint-reuse Provides-Extra: ports-pygame Provides-Extra: ports-rtmidi Provides-Extra: ports-rtmidi-python Provides-Extra: release Provides-Extra: test-code Provides-Extra: dev Provides-Extra: ports-all License-File: LICENSE .. SPDX-FileCopyrightText: 2013 Ole Martin Bjorndalen .. .. SPDX-License-Identifier: CC-BY-4.0 Mido - MIDI Objects for Python ============================== .. image:: https://img.shields.io/badge/License-MIT-blue.svg :alt: MIT License :target: https://github.com/mido/mido/blob/main/LICENSES/MIT.txt .. image:: https://img.shields.io/pypi/v/mido.svg :alt: PyPi version :target: https://pypi.org/project/mido .. image:: https://img.shields.io/pypi/pyversions/mido.svg :alt: Python version :target: https://python.org .. image:: https://pepy.tech/badge/mido :alt: Downloads :target: https://pepy.tech/project/mido .. image:: https://github.com/mido/mido/workflows/Test/badge.svg :alt: Test status :target: https://github.com/mido/mido/actions .. image:: https://readthedocs.org/projects/mido/badge/?version=latest :alt: Docs status :target: https://mido.readthedocs.io/ .. image:: https://api.reuse.software/badge/github.com/mido/mido :alt: REUSE status :target: https://api.reuse.software/info/github.com/mido/mido .. image:: https://www.bestpractices.dev/projects/7987/badge :alt: OpenSSF Best Practices :target: https://www.bestpractices.dev/projects/7987 Mido is a library for working with MIDI messages and ports: .. code-block:: python >>> import mido >>> msg = mido.Message('note_on', note=60) >>> msg.type 'note_on' >>> msg.note 60 >>> msg.bytes() [144, 60, 64] >>> msg.copy(channel=2) Message('note_on', channel=2, note=60, velocity=64, time=0) .. code-block:: python port = mido.open_output('Port Name') port.send(msg) .. code-block:: python with mido.open_input() as inport: for msg in inport: print(msg) .. code-block:: python mid = mido.MidiFile('song.mid') for msg in mid.play(): port.send(msg) Full documentation at https://mido.readthedocs.io/ Main Features ------------- * convenient message objects. * supports RtMidi, PortMidi and Pygame. New backends are easy to write. * full support for all 18 messages defined by the MIDI standard. * standard port API allows all kinds of input and output ports to be used interchangeably. New port types can be written by subclassing and overriding a few methods. * includes a reusable MIDI stream parser. * full support for MIDI files (read, write, create and play) with complete access to every message in the file, including all common meta messages. * can read and write SYX files (binary and plain text). * implements (somewhat experimental) MIDI over TCP/IP with socket ports. This allows for example wireless MIDI between two computers. * includes programs for playing MIDI files, listing ports and serving and forwarding ports over a network. Status ------ 1.3 is the fourth stable release. This project uses `Semantic Versioning `_. Requirements ------------ Mido requires Python 3.7 or higher. Installing ---------- :: python3 -m pip install mido Or, alternatively, if you want to use ports with the default backend:: python3 -m pip install mido[ports-rtmidi] See ``docs/backends/`` for other backends. Source Code ----------- https://github.com/mido/mido/ License ------- Mido is released under the terms of the `MIT license `_. Questions and suggestions ------------------------- For questions and proposals which may not fit into issues or pull requests, we recommend to ask and discuss in the `Discussions `_ section. mido-1.3.3/mido.egg-info/SOURCES.txt0000644000175100001770000000673214706731615017603 0ustar runnerdocker00000000000000.gitignore .readthedocs.yaml LICENSE MANIFEST.in README.rst pyproject.toml .github/dependabot.yml .github/ISSUE_TEMPLATE/bug_report.md .github/ISSUE_TEMPLATE/bug_report.md.license .github/ISSUE_TEMPLATE/feature_request.md .github/ISSUE_TEMPLATE/feature_request.md.license .github/workflows/documentation.yml .github/workflows/release.yml .github/workflows/test.yml .reuse/dep5 LICENSES/CC-BY-4.0.txt LICENSES/CC-BY-SA-3.0.txt LICENSES/CC0-1.0.txt LICENSES/MIT.txt docs/CODE_OF_CONDUCT.rst docs/Makefile docs/about_midi.rst docs/acknowledgements.rst docs/api.rst docs/authors.rst docs/binaries.rst docs/changes.rst docs/conf.py docs/contributing.rst docs/freezing_exe.rst docs/glossary.rst docs/index.rst docs/installing.rst docs/intro.rst docs/licenses.rst docs/make.bat docs/message_types.rst docs/meta_message_types.rst docs/resources.rst docs/_static/.gitkeep docs/backends/amidi.rst docs/backends/custom.rst docs/backends/index.rst docs/backends/portmidi.rst docs/backends/pygame.rst docs/backends/rtmidi.rst docs/backends/rtmidi_python.rst docs/files/index.rst docs/files/midi.rst docs/files/syx.rst docs/images/midi_time.svg docs/images/midi_time.svg.license docs/messages/frozen.rst docs/messages/index.rst docs/messages/parsing.rst docs/messages/serializing.rst docs/ports/custom.rst docs/ports/index.rst docs/ports/socket.rst docs/shared/licenses_logos.rst examples/using_rtmidi_directly.py examples/backends/printer.py examples/backends/rtm.py examples/backends/use_printer.py examples/midifiles/create_midi_file.py examples/midifiles/midifile_to_json.py examples/midifiles/play_midi_file.py examples/midifiles/print_midi_file.py examples/midifiles/show_midifile.py examples/midifiles/test.sh examples/ports/input_filter.py examples/ports/list_ports.py examples/ports/multi_receive.py examples/ports/nonblocking_receive.py examples/ports/queue_port.py examples/ports/receive.py examples/ports/send.py examples/sockets/forward_ports.py examples/sockets/serve_ports.py examples/sockets/simple_client.py examples/sockets/simple_server.py extras/README.rst extras/hid_joystick.py mido/__init__.py mido/frozen.py mido/parser.py mido/ports.py mido/sockets.py mido/syx.py mido/tokenizer.py mido/version.py mido.egg-info/PKG-INFO mido.egg-info/SOURCES.txt mido.egg-info/dependency_links.txt mido.egg-info/entry_points.txt mido.egg-info/requires.txt mido.egg-info/top_level.txt mido/backends/__init__.py mido/backends/_parser_queue.py mido/backends/amidi.py mido/backends/backend.py mido/backends/portmidi.py mido/backends/portmidi_init.py mido/backends/pygame.py mido/backends/rtmidi.py mido/backends/rtmidi_python.py mido/backends/rtmidi_utils.py mido/messages/__init__.py mido/messages/checks.py mido/messages/decode.py mido/messages/encode.py mido/messages/messages.py mido/messages/specs.py mido/messages/strings.py mido/midifiles/__init__.py mido/midifiles/meta.py mido/midifiles/midifiles.py mido/midifiles/tracks.py mido/midifiles/units.py mido/scripts/__init__.py mido/scripts/mido_connect.py mido/scripts/mido_play.py mido/scripts/mido_ports.py mido/scripts/mido_serve.py tests/test_frozen.py tests/test_parser.py tests/test_ports.py tests/test_sockets.py tests/test_syx.py tests/test_tokenizer.py tests/backends/test_backend.py tests/backends/test_rtmidi.py tests/messages/test_checks.py tests/messages/test_decode.py tests/messages/test_encode.py tests/messages/test_messages.py tests/messages/test_strings.py tests/midifiles/test_meta.py tests/midifiles/test_midifiles.py tests/midifiles/test_tracks.py tests/midifiles/test_units.pymido-1.3.3/mido.egg-info/dependency_links.txt0000644000175100001770000000000114706731615021755 0ustar runnerdocker00000000000000 mido-1.3.3/mido.egg-info/entry_points.txt0000644000175100001770000000027414706731615021210 0ustar runnerdocker00000000000000[console_scripts] mido-connect = mido.scripts.mido_connect:main mido-play = mido.scripts.mido_play:main mido-ports = mido.scripts.mido_ports:main mido-serve = mido.scripts.mido_serve:main mido-1.3.3/mido.egg-info/requires.txt0000644000175100001770000000104014706731615020302 0ustar runnerdocker00000000000000packaging [:python_version < "3.8"] importlib_metadata [build-docs] sphinx~=4.3.2 sphinx-rtd-theme~=1.2.2 [check-manifest] check-manifest>=0.49 [dev] mido[check-manifest] mido[lint-code] mido[test-code] mido[lint-reuse] mido[build-docs] mido[release] [lint-code] ruff~=0.1.6 [lint-reuse] reuse~=1.1.2 [ports-all] mido[ports-pygame] mido[ports-rtmidi] mido[ports-rtmidi-python] [ports-pygame] PyGame~=2.5 [ports-rtmidi] python-rtmidi~=1.5.4 [ports-rtmidi-python] rtmidi-python~=0.2.2 [release] twine~=4.0.2 [test-code] pytest~=7.4.0 mido-1.3.3/mido.egg-info/top_level.txt0000644000175100001770000000000514706731615020434 0ustar runnerdocker00000000000000mido mido-1.3.3/pyproject.toml0000644000175100001770000001013214706731605016175 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2023 Ole Martin Bjorndalen # SPDX-FileCopyrightText: 2023 Raphaël Doursenaud # # SPDX-License-Identifier: CC0-1.0 # PEP 621 (https://peps.python.org/pep-0621/) project metadata # Format: https://toml.io/en/ [build-system] requires = ["setuptools>=61.0.0", "setuptools-scm"] build-backend = "setuptools.build_meta" [project] name = "mido" # version is dynamic description = "MIDI Objects for Python" readme = "README.rst" requires-python = "~=3.7" license = { text = "MIT" } authors = [ { name = "Ole Martin Bjorndalen", email = "ombdalen@gmail.com" }, ] maintainers = [ { name = "Radovan Bast", email = "radovan.bast@gmail.com" }, { name = "Raphaël Doursenaud", email = "rdoursenaud@gmail.com" }, ] keywords = ["python", "midi", "midifile"] classifiers = [ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'Natural Language :: English', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python', 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13', 'Topic :: Multimedia :: Sound/Audio :: MIDI', 'Topic :: Software Development', 'Topic :: Software Development :: Libraries', 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Utilities', ] dependencies = [ "importlib_metadata; python_version<'3.8'", # For version introspection "packaging", # To provide a nice version_info opbject ] dynamic = ["version"] [project.urls] documentation = "https://mido.readthedocs.io" source = "https://github.com/mido/mido" [project.optional-dependencies] # Separated by tasks for automation efficiency build-docs = [ 'sphinx~=4.3.2', 'sphinx-rtd-theme~=1.2.2', ] check-manifest = ['check-manifest>=0.49',] lint-code = ['ruff~=0.1.6',] lint-reuse = ['reuse~=1.1.2',] ports-pygame = ['PyGame~=2.5',] ports-rtmidi = ['python-rtmidi~=1.5.4',] ports-rtmidi-python = ['rtmidi-python~=0.2.2',] release = ['twine~=4.0.2',] test-code = ['pytest~=7.4.0',] # Convenience groups for human interaction dev = [ 'mido[check-manifest]', 'mido[lint-code]', 'mido[test-code]', 'mido[lint-reuse]', 'mido[build-docs]', 'mido[release]', ] ports-all = [ 'mido[ports-pygame]', 'mido[ports-rtmidi]', 'mido[ports-rtmidi-python]', ] [project.scripts] mido-play = "mido.scripts.mido_play:main" mido-ports = "mido.scripts.mido_ports:main" mido-serve = "mido.scripts.mido_serve:main" mido-connect = "mido.scripts.mido_connect:main" [tool.setuptools] packages = [ "mido", "mido.backends", "mido.messages", "mido.midifiles", "mido.scripts", ] include-package-data = true [tool.setuptools.package-dir] mido = "mido" [tool.setuptools.package-data] mido = [ "LICENSE", ] [tool.setuptools_scm] # Enables setuptools-scm which updates the version file `mido/_version.py` # automatically based on git tags. # The version information can be retrieved using: # ```python # importlib_metadata.version('mido') # ``` [tool.pytest.ini_options] minversion = "6.0" addopts = "-rs --quiet" testpaths = [ "tests", ] norecursedirs = [ "build", "dist", "examples", ] [tool.ruff] ignore = [ "F401", # TODO: Enable ] extend-select = [ "B", "E", "F", "I", "S", # security lints "W", "T", ] [tool.ruff.per-file-ignores] "tests/**" = [ "S101", # allow assertions in tests ] "extras/**" = [ "T201", # print allowed ] "examples/**" = [ "B007", # allow slightly sloppy loop variables in examples "S311", # allow RNGs that are not cryptographically secure "T201", # print allowed ] "mido/backends/amidi.py" = [ "S603", # allow subprocesses with possibly untrusted input "S607", # allow `amidi` as a partial path ] "mido/scripts/**" = [ "T201", # print allowed ] mido-1.3.3/setup.cfg0000644000175100001770000000004614706731615015106 0ustar runnerdocker00000000000000[egg_info] tag_build = tag_date = 0 mido-1.3.3/tests/0000755000175100001770000000000014706731615014427 5ustar runnerdocker00000000000000mido-1.3.3/tests/backends/0000755000175100001770000000000014706731615016201 5ustar runnerdocker00000000000000mido-1.3.3/tests/backends/test_backend.py0000644000175100001770000000056414706731605021205 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2017 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT from mido.backends.backend import Backend def test_split_api(): backend = Backend('test') assert backend.name == 'test' assert backend.api is None backend = Backend('test/ALSA') assert backend.name == 'test' assert backend.api == 'ALSA' mido-1.3.3/tests/backends/test_rtmidi.py0000644000175100001770000000154614706731605021107 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2017 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT from mido.backends.rtmidi_utils import expand_alsa_port_name def test_expand_alsa_port_name(): port_names = sorted(['A:port 128:0', 'B:port 129:0', 'B:port 129:0', 'Z:port 130:0']) def expand(name): return expand_alsa_port_name(port_names, name) # Should return first matching port. assert expand('port') == 'A:port 128:0' assert expand('A:port') == 'A:port 128:0' assert expand('B:port') == 'B:port 129:0' # Full name should also work. assert expand('A:port 128:0') == 'A:port 128:0' # If the port is not found the original name should be returned # for the caller to deal with. assert expand('invalid name') == 'invalid name' mido-1.3.3/tests/messages/0000755000175100001770000000000014706731615016236 5ustar runnerdocker00000000000000mido-1.3.3/tests/messages/test_checks.py0000644000175100001770000000071414706731605021110 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2017 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT from pytest import raises from mido.messages.checks import check_time def test_check_time(): check_time(1) check_time(1.5) with raises(TypeError): check_time(None) with raises(TypeError): check_time('abc') with raises(TypeError): check_time(None) with raises(TypeError): check_time('abc') mido-1.3.3/tests/messages/test_decode.py0000644000175100001770000000214014706731605021066 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2017 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT from pytest import raises from mido.messages.decode import decode_message def sysex(data): """Make sysex data.""" return [0xf0] + list(data) + [0xf7] def test_sysex(): data = b'\xf0\x01\x02\x03\xf7' msg = {'type': 'sysex', 'data': (1, 2, 3), 'time': 0} assert decode_message(data) == msg def test_channel(): assert decode_message(b'\x91\x00\x00')['channel'] == 1 def test_sysex_end(): with raises(ValueError): decode_message(b'\xf0\x00\x12') def test_zero_bytes(): with raises(ValueError): decode_message(b'') def test_too_few_bytes(): with raises(ValueError): decode_message(b'\x90') def test_too_many_bytes(): with raises(ValueError): decode_message(b'\x90\x00\x00\x00') def test_invalid_status(): with raises(ValueError): decode_message(b'\x00') def test_sysex_without_stop_byte(): with raises(ValueError): decode_message([0xf0]) with raises(ValueError): decode_message([0xf0, 0]) mido-1.3.3/tests/messages/test_encode.py0000644000175100001770000000143114706731605021102 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2017 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT from mido.messages.decode import decode_message from mido.messages.encode import encode_message from mido.messages.specs import SPEC_BY_STATUS def test_encode_decode_all(): """Encode and then decode all messages on all channels. Each data byte is different so that the test will fail if the bytes are swapped during encoding or decoding. """ data_bytes = [1, 2, 3] for status_byte, spec in SPEC_BY_STATUS.items(): if status_byte == 0xf0: msg_bytes = [0xf0] + data_bytes + [0xf7] else: msg_bytes = [status_byte] + data_bytes[:spec['length'] - 1] assert encode_message(decode_message(msg_bytes)) == msg_bytes mido-1.3.3/tests/messages/test_messages.py0000644000175100001770000000675514706731605021472 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2017 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT from pytest import raises from mido.messages.messages import Message, SysexData from mido.messages.specs import MAX_PITCHWHEEL, MAX_SONGPOS, MIN_PITCHWHEEL, MIN_SONGPOS def test_msg_time_equality(): # Since 1.1.18 time is included in comparison. assert Message('clock', time=0) == Message('clock', time=0) assert Message('clock', time=0) != Message('clock', time=1) def test_set_type(): """Can't change the type of a message.""" with raises(AttributeError): Message('note_on').type = 'note_off' def test_encode_pitchwheel(): assert 'E0 00 00' == Message('pitchwheel', pitch=MIN_PITCHWHEEL).hex() assert 'E0 00 40' == Message('pitchwheel', pitch=0).hex() assert 'E0 7F 7F' == Message('pitchwheel', pitch=MAX_PITCHWHEEL).hex() def test_decode_pitchwheel(): assert Message.from_hex('E0 00 00').pitch == MIN_PITCHWHEEL assert Message.from_hex('E0 00 40').pitch == 0 assert Message.from_hex('E0 7F 7F').pitch == MAX_PITCHWHEEL def test_encode_songpos(): assert 'F2 00 00' == Message('songpos', pos=MIN_SONGPOS).hex() assert 'F2 7F 7F' == Message('songpos', pos=MAX_SONGPOS).hex() def test_decode_songpos(): assert Message.from_hex('F2 00 00').pos == MIN_SONGPOS assert Message.from_hex('F2 7F 7F').pos == MAX_SONGPOS def test_sysex_data_is_sysexdata_object(): assert isinstance(Message.from_hex('F0 00 F7').data, SysexData) def test_sysex_data_accepts_different_types(): assert Message('sysex', data=(0, 1, 2)).data == (0, 1, 2) assert Message('sysex', data=[0, 1, 2]).data == (0, 1, 2) assert Message('sysex', data=range(3)).data == (0, 1, 2) assert Message('sysex', data=bytearray([0, 1, 2])).data == (0, 1, 2) assert Message('sysex', data=b'\x00\x01\x02').data == (0, 1, 2) def test_copy(): assert Message('start').copy(time=1) == Message('start', time=1) def test_init_invalid_argument(): with raises(ValueError): Message('note_on', zzzzzzzzzzzz=2) with raises(ValueError): # note_on doesn't take program. Message('note_on', program=2) def test_copy_invalid_argument(): with raises(ValueError): Message('note_on').copy(zzzzzzzzzzzz=2) with raises(ValueError): # note_on doesn't take program. Message('note_on').copy(program=2) def test_copy_cant_change_type(): with raises(ValueError): Message('start').copy(type='stop') def test_copy_can_have_same_type(): Message('start').copy(type='start') def test_copy_handles_data_generator(): msg1 = Message('sysex') msg2 = msg1.copy(data=(i for i in range(3))) assert msg2.data == (0, 1, 2) assert isinstance(msg2.data, SysexData) def test_compare_with_nonmessage(): with raises(TypeError): assert Message('clock') == 'not a message' def test_from_dict_default_values(): msg = Message('note_on', channel=0, note=0, time=0) data = {'type': 'note_on'} assert Message.from_dict(data) == msg def test_dict_sysex_data(): msg = Message('sysex', data=(1, 2, 3)) data = msg.dict() assert data == {'type': 'sysex', 'data': [1, 2, 3], 'time': 0} assert isinstance(data['data'], list) def test_from_hex_sysex_data_type(): msg = Message.from_hex('F0 01 02 03 F7') assert isinstance(msg.data, SysexData) def test_repr(): msg = Message('note_on', channel=1, note=2, time=3) msg_eval = eval(repr(msg)) # noqa: S307 assert msg == msg_eval mido-1.3.3/tests/messages/test_strings.py0000644000175100001770000000124614706731605021342 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2017 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT from pytest import raises from mido.messages import Message def test_decode_sysex(): assert Message.from_str('sysex data=(1,2,3)').data == (1, 2, 3) def test_decode_invalid_sysex_with_spaces(): with raises(ValueError): Message.from_str('sysex data=(1, 2, 3)') def test_encode_sysex(): assert str(Message('sysex', data=())) == 'sysex data=() time=0' # This should not have an extra comma. assert str(Message('sysex', data=(1,))) == 'sysex data=(1) time=0' assert str(Message('sysex', data=(1, 2, 3))) == 'sysex data=(1,2,3) time=0' mido-1.3.3/tests/midifiles/0000755000175100001770000000000014706731615016374 5ustar runnerdocker00000000000000mido-1.3.3/tests/midifiles/test_meta.py0000644000175100001770000000567614706731605020750 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2017 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT import pytest from mido.midifiles.meta import ( KeySignatureError, MetaMessage, MetaSpec_key_signature, UnknownMetaMessage, ) def test_copy_invalid_argument(): with pytest.raises(ValueError): MetaMessage('track_name').copy(a=1) def test_copy_cant_override_type(): with pytest.raises(ValueError): MetaMessage('track_name').copy(type='end_of_track') class TestKeySignature: @pytest.mark.parametrize('bad_key_sig', [[8, 0], [8, 1], [0, 2], [9, 1], [255 - 7, 0]]) def test_bad_key_sig_throws_key_signature_error(self, bad_key_sig): with pytest.raises(KeySignatureError): MetaSpec_key_signature().decode(MetaMessage('key_signature'), bad_key_sig) @pytest.mark.parametrize('input_bytes,expect_sig', [([0, 0], 'C'), ([0, 1], 'Am'), ([255 - 6, 0], 'Cb'), ([255 - 6, 1], 'Abm'), ([7, 1], 'A#m') ]) def test_key_signature(self, input_bytes, expect_sig): msg = MetaMessage('key_signature') MetaSpec_key_signature().decode(msg, input_bytes) assert msg.key == expect_sig def test_meta_message_repr(): msg = MetaMessage('end_of_track', time=10) msg_eval = eval(repr(msg)) # noqa: S307 assert msg == msg_eval def test_unknown_meta_message_repr(): msg = UnknownMetaMessage(type_byte=99, data=[1, 2], time=10) msg_eval = eval(repr(msg)) # noqa: S307 assert msg == msg_eval def test_meta_from_bytes_invalid(): test_bytes = [ 0xC0, # Not a meta event (Program Change channel 1) 0x05 # Program #5 ] with pytest.raises(ValueError): MetaMessage.from_bytes(test_bytes) def test_meta_from_bytes_data_too_short(): test_bytes = [ 0xFF, # Meta event 0x01, # Event Type: Text 0x04, # Length ord('T'), ord('E'), ord('S'), # Text: TES ] with pytest.raises(ValueError): MetaMessage.from_bytes(test_bytes) def test_meta_from_bytes_data_too_long(): test_bytes = [ 0xFF, # Meta event 0x01, # Event Type: Text 0x04, # Length ord('T'), ord('E'), ord('S'), ord('T'), ord('S') # Text: TESTS ] with pytest.raises(ValueError): MetaMessage.from_bytes(test_bytes) def test_meta_from_bytes_text(): test_bytes = [ 0xFF, # Meta event 0x01, # Event Type: Text 0x04, # Length ord('T'), ord('E'), ord('S'), ord('T') # Text: TEST ] msg = MetaMessage.from_bytes(test_bytes) assert msg.type == 'text' assert msg.text == 'TEST' mido-1.3.3/tests/midifiles/test_midifiles.py0000644000175100001770000001255214706731605021756 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2017 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT import io from pytest import raises from mido.messages import Message from mido.midifiles.meta import KeySignatureError, MetaMessage from mido.midifiles.midifiles import MidiFile, MidiTrack HEADER_ONE_TRACK = """ 4d 54 68 64 # MThd 00 00 00 06 # Chunk size 00 01 # Type 1 00 01 # 1 track 00 78 # 120 ticks per beat """ def parse_hexdump(hexdump): data = bytearray() for line in hexdump.splitlines(): data += bytearray.fromhex(line.split('#')[0]) return data def read_file(hexdump, clip=False): return MidiFile(file=io.BytesIO(parse_hexdump(hexdump)), clip=clip) def test_no_tracks(): assert read_file(""" 4d 54 68 64 # MThd 00 00 00 06 # Chunk size 00 01 # Type 1 00 00 # 0 tracks 00 78 # 120 ticks per beat """).tracks == [] def test_single_message(): assert read_file(HEADER_ONE_TRACK + """ 4d 54 72 6b # MTrk 00 00 00 04 20 90 40 40 # note_on """).tracks[0] == [Message('note_on', note=64, velocity=64, time=32)] def test_too_long_message(): with raises(IOError): read_file(HEADER_ONE_TRACK + """ 4d 54 72 6b # MTrk 00 00 00 04 00 ff 03 ff ff 7f # extremely long track name message """) def test_two_tracks(): mid = read_file(""" 4d54 6864 0000 0006 0001 0002 0040 # Header 4d54 726b 0000 0008 00 90 40 10 40 80 40 10 # Track 0 4d54 726b 0000 0008 00 90 47 10 40 80 47 10 # Track 1 """) assert len(mid.tracks) == 2 # TODO: add some more tests here. def test_empty_file(): with raises(EOFError): read_file("") def test_eof_in_track(): with raises(EOFError): read_file(HEADER_ONE_TRACK + """ 4d 54 72 6b # MTrk 00 00 00 01 # Chunk size # Oops, no data here. """) def test_invalid_data_byte_no_clipping(): # TODO: should this raise IOError? with raises(IOError): read_file(HEADER_ONE_TRACK + """ 4d 54 72 6b # MTrk 00 00 00 04 # Chunk size 00 90 ff 40 # note_on note=255 velocity=64 """) def test_invalid_data_byte_with_clipping_high(): midi_file = read_file(HEADER_ONE_TRACK + """ 4d 54 72 6b # MTrk 00 00 00 04 # Chunk size 00 90 ff 40 # note_on note=255 velocity=64 """, clip=True) assert midi_file.tracks[0][0].note == 127 def test_meta_messages(): # TODO: should this raise IOError? mid = read_file(HEADER_ONE_TRACK + """ 4d 54 72 6b # MTrk 00 00 00 0c # Chunk size 00 ff 03 04 54 65 73 74 # track_name name='Test' 00 ff 2f 00 # end_of_track """) track = mid.tracks[0] assert track[0] == MetaMessage('track_name', name='Test') assert track[1] == MetaMessage('end_of_track') def test_meta_message_bad_key_sig_throws_key_signature_error_sharps(): with raises(KeySignatureError): read_file(HEADER_ONE_TRACK + """ 4d 54 72 6b # MTrk 00 00 00 09 # Chunk size 00 ff 59 02 08 # Key Signature with 8 sharps 00 ff 2f 00 # end_of_track """) def test_meta_message_bad_key_sig_throws_key_signature_error_flats(): with raises(KeySignatureError): read_file(HEADER_ONE_TRACK + """ 4d 54 72 6b # MTrk 00 00 00 09 # Chunk size 00 ff 59 02 f8 # Key Signature with 8 flats 00 ff 2f 00 # end_of_track """) def test_meta_messages_with_length_0(): """sequence_number and midi_port with no data bytes should be accepted. In rare cases these messages have length 0 and thus no data bytes. (See issues 42 and 93.) It is unclear why these messages are missing their data. It could be cased by a bug in the software that created the files. So far this has been fixed by adding a test to each of these two meta message classes. If the problem appears with other message types it may be worth finding a more general solution. """ mid = read_file(HEADER_ONE_TRACK + """ 4d 54 72 6b # MTrk 00 00 00 17 00 ff 00 00 # sequence_number with no data bytes (should be 2). 00 ff 21 00 # midi_port with no data bytes (should be 1). 00 ff 00 02 00 01 # sequence_number with correct number of data bytes (2). 00 ff 21 01 01 # midi_port with correct number of data bytes (1). 00 ff 2f 00 """) assert mid.tracks[0] == [ MetaMessage('sequence_number', number=0), MetaMessage('midi_port', port=0), MetaMessage('sequence_number', number=1), MetaMessage('midi_port', port=1), MetaMessage('end_of_track'), ] def test_midifile_repr(): midifile = MidiFile(type=1, ticks_per_beat=123, tracks=[ MidiTrack([ Message('note_on', channel=1, note=2, time=3), Message('note_off', channel=1, note=2, time=3)]), MidiTrack([ MetaMessage('sequence_number', number=5), Message('note_on', channel=2, note=6, time=9), Message('note_off', channel=2, note=6, time=9)]), ]) midifile_eval = eval(repr(midifile)) # noqa: S307 for track, track_eval in zip(midifile.tracks, midifile_eval.tracks): for m1, m2 in zip(track, track_eval): assert m1 == m2 mido-1.3.3/tests/midifiles/test_tracks.py0000644000175100001770000000442114706731605021274 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2017 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT import itertools import time import mido from mido.messages import Message from mido.midifiles.meta import MetaMessage, UnknownMetaMessage from mido.midifiles.tracks import MidiTrack zip = getattr(itertools, 'izip', zip) def test_track_slice(): track = MidiTrack() # Slice should return MidiTrack object. assert isinstance(track[::], MidiTrack) def test_track_name(): name1 = MetaMessage('track_name', name='name1') name2 = MetaMessage('track_name', name='name2') # The track should use the first name it finds. track = MidiTrack([name1, name2]) assert track.name == name1.name def test_track_repr(): track = MidiTrack([ Message('note_on', channel=1, note=2, time=3), Message('note_off', channel=1, note=2, time=3), ]) track_eval = eval(repr(track)) # noqa: S307 for m1, m2 in zip(track, track_eval): assert m1 == m2 def test_merge_large_midifile(): mid = mido.MidiFile() for k in range(5): t = mido.MidiTrack() for _ in range(10000): t.append(mido.Message("note_on", note=72, time=1000 + 100 * k)) t.append(mido.Message("note_off", note=72, time=500 + 100 * k)) mid.tracks.append(t) # Add meta messages for testing. meta1 = mido.MetaMessage('track_name', name='Test Track 1') meta2 = mido.MetaMessage('track_name', name='Test Track 2') meta3 = mido.MetaMessage('time_signature', numerator=4, denominator=4, clocks_per_click=24, notated_32nd_notes_per_beat=8) unknown_meta = mido.UnknownMetaMessage(0x50, b'\x01\x02\x03') mid.tracks[0].insert(0, meta1) mid.tracks[1].insert(0, meta2) mid.tracks[2].insert(0, meta3) mid.tracks[3].insert(0, unknown_meta) start = time.time() merged = list(mido.merge_tracks(mid.tracks, skip_checks=True)) finish = time.time() merged_duration_ticks = sum(msg.time for msg in merged) max_track_duration_ticks = max( sum(msg.time for msg in t) for t in mid.tracks) assert merged_duration_ticks == max_track_duration_ticks assert (finish - start) < 3.0 mido-1.3.3/tests/midifiles/test_units.py0000644000175100001770000000702314706731605021150 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2017 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT from mido.midifiles.units import bpm2tempo, second2tick, tempo2bpm, tick2second def test_tempo2bpm(): # default tempo (500000 ms per quarter note) assert tempo2bpm(500000) == 120 assert tempo2bpm(1000000) == 60 # x/4 time signature: 4 beats (quarter notes) per bar # 60 bpm: 1 beat / sec = 1 quarter note / sec: 1 sec / quarter note assert tempo2bpm(1000000, time_signature=(4, 4)) == 60 # 120 bpm: 2 beat / sec = 2 quarter note / sec: 0.5 sec / quarter note assert tempo2bpm(500000, time_signature=(4, 4)) == 120 assert tempo2bpm(500000, time_signature=(3, 4)) == 120 # 3/4 is the same # x/2 time signature: 2 beats (half notes) per bar # 60 bpm: 1 beat / sec = 2 quarter note / sec: 0.5 sec / quarter note assert tempo2bpm(500000, time_signature=(2, 2)) == 60 # 120 bpm: 2 beat / sec = 4 quarter note / sec: 0.25 sec / quarter note assert tempo2bpm(250000, time_signature=(2, 2)) == 120 assert tempo2bpm(250000, time_signature=(3, 2)) == 120 # 3/2 is the same # x/8 time signature: 8 beats (eighth notes) per bar # 60 bpm: 1 beat / sec = 0.5 quarter note / sec: 2 sec / quarter note assert tempo2bpm(2000000, time_signature=(8, 8)) == 60 # 120 bpm: 2 beat / sec = 1 quarter note / sec: 1 sec / quarter note assert tempo2bpm(1000000, time_signature=(8, 8)) == 120 assert tempo2bpm(1000000, time_signature=(8, 8)) == 120 # 6/8 is the same def test_bpm2tempo(): # default tempo (500000 ms per quarter note) assert bpm2tempo(60) == 1000000 assert bpm2tempo(120) == 500000 # x/4 time signature: 4 beats (quarter notes) per bar # 60 bpm: 1 beat / sec = 1 quarter note / sec: 1 sec / quarter note assert bpm2tempo(60, time_signature=(4, 4)) == 1000000 # 120 bpm: 2 beat / sec = 2 quarter note / sec: 0.5 sec / quarter note assert bpm2tempo(120, time_signature=(4, 4)) == 500000 assert bpm2tempo(120, time_signature=(3, 4)) == 500000 # 3/4 is the same # x/2 time signature: 2 beats (half notes) per bar # 60 bpm: 1 beat / sec = 2 quarter note / sec: 0.5 sec / quarter note assert bpm2tempo(60, time_signature=(2, 2)) == 500000 # 120 bpm: 2 beat / sec = 4 quarter note / sec: 0.25 sec / quarter note assert bpm2tempo(120, time_signature=(2, 2)) == 250000 assert bpm2tempo(120, time_signature=(3, 2)) == 250000 # 3/2 is the same # x/8 time signature: 8 beats (eighth notes) per bar # 60 bpm: 1 beat / sec = 0.5 quarter note / sec: 2 sec / quarter note assert bpm2tempo(60, time_signature=(8, 8)) == 2000000 # 120 bpm: 2 beat / sec = 1 quarter note / sec: 1 sec / quarter note assert bpm2tempo(120, time_signature=(8, 8)) == 1000000 assert bpm2tempo(120, time_signature=(8, 8)) == 1000000 # 6/8 is the same # TODO: these tests could be improved with better test values such as # edge cases. def test_tick2second(): # default tempo (500000 ms per quarter note) assert tick2second(1, ticks_per_beat=100, tempo=500000) == 0.005 assert tick2second(2, ticks_per_beat=100, tempo=100000) == 0.002 def test_second2tick(): # default tempo (500000 ms per quarter note) assert second2tick(0.001, ticks_per_beat=100, tempo=500000) == 0 assert second2tick(0.004, ticks_per_beat=100, tempo=500000) == 1 assert second2tick(0.005, ticks_per_beat=100, tempo=500000) == 1 assert second2tick(0.0015, ticks_per_beat=100, tempo=100000) == 2 assert second2tick(0.0025, ticks_per_beat=100, tempo=100000) == 2 mido-1.3.3/tests/test_frozen.py0000644000175100001770000000327214706731605017346 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2017 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT from mido.frozen import ( FrozenMessage, FrozenMetaMessage, FrozenUnknownMetaMessage, freeze_message, is_frozen, thaw_message, ) from mido.messages import Message from mido.midifiles.meta import UnknownMetaMessage def test_hashability(): """Test that messages are hashable.""" hash(FrozenMessage('note_on')) # List is converted to tuple. hash(FrozenMessage('sysex', data=[1, 2, 3])) hash(FrozenMetaMessage('track_name', name='Some track')) hash(FrozenUnknownMetaMessage(123, [1, 2, 3])) def test_freeze_and_thaw(): """Test that messages are hashable.""" assert not is_frozen(thaw_message(freeze_message(Message('note_on')))) def test_thawed_message_is_copy(): frozen_msg = FrozenMessage('note_on') thawed_msg = Message('note_on') assert thaw_message(frozen_msg) == thawed_msg def test_is_frozen(): assert is_frozen(FrozenMessage('note_on')) assert not is_frozen(Message('note_on')) def test_frozen_repr(): msg = FrozenMessage('note_on', channel=1, note=2, time=3) msg_eval = eval(repr(msg)) # noqa: S307 assert isinstance(msg_eval, FrozenMessage) assert msg == msg_eval def test_frozen_meta_repr(): msg = FrozenMetaMessage('end_of_track', time=10) msg_eval = eval(repr(msg)) # noqa: S307 assert isinstance(msg_eval, FrozenMetaMessage) assert msg == msg_eval def test_frozen_unknown_meta_repr(): msg = FrozenUnknownMetaMessage(type_byte=99, data=[1, 2], time=10) msg_eval = eval(repr(msg)) # noqa: S307 assert isinstance(msg_eval, UnknownMetaMessage) assert msg == msg_eval mido-1.3.3/tests/test_parser.py0000644000175100001770000000654214706731605017342 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2017 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT import random from pytest import raises from mido.messages import Message, specs from mido.parser import Parser, parse, parse_all def test_parse(): """Parse a note_on msg and compare it to one created with Message().""" parsed = parse(b'\x90\x4c\x20') other = Message('note_on', channel=0, note=0x4c, velocity=0x20) assert parsed == other def test_parse_stray_data(): """The parser should ignore stray data bytes.""" assert parse_all(b'\x20\x30') == [] def test_parse_stray_status_bytes(): """The parser should ignore stray status bytes.""" assert parse_all(b'\x90\x90\xf0') == [] def test_encode_and_parse(): """Encode a message and then parse it. Should return the same message. """ note_on = Message('note_on') assert note_on == parse(note_on.bytes()) def test_feed_byte(): """Put various things into feed_byte().""" parser = Parser() parser.feed_byte(0) parser.feed_byte(255) with raises(TypeError): parser.feed_byte([1, 2, 3]) with raises(ValueError): parser.feed_byte(-1) with raises(ValueError): parser.feed_byte(256) def test_feed(): """Put various things into feed().""" parser = Parser() parser.feed([]) parser.feed([1, 2, 3]) # TODO: add more valid types. with raises(TypeError): parser.feed(1) with raises(TypeError): parser.feed(None) with raises(TypeError): parser.feed() def test_parse_random_bytes(): """Parser should not crash when parsing random data.""" randrange = random.Random('a_random_seed').randrange parser = Parser() for _ in range(10000): byte = randrange(256) parser.feed_byte(byte) def test_parse_channel(): """Parser should not discard the channel in channel messages.""" assert parse([0x90, 0x00, 0x00]).channel == 0 assert parse([0x92, 0x00, 0x00]).channel == 2 def test_one_byte_message(): """Messages that are one byte long should not wait for data bytes.""" messages = parse_all([0xf6]) # Tune request. assert len(messages) == 1 assert messages[0].type == 'tune_request' def test_undefined_messages(): """The parser should ignore undefined status bytes and sysex_end.""" messages = parse_all([0xf4, 0xf5, 0xf7, 0xf9, 0xfd]) assert messages == [] def test_realtime_inside_sysex(): """Realtime message inside sysex should be delivered first.""" messages = parse_all([0xf0, 0, 0xfb, 0, 0xf7]) assert len(messages) == 2 assert messages[0].type == 'continue' assert messages[1].type == 'sysex' def test_undefined_realtime_inside_sysex(): """Undefined realtime message inside sysex should ignored.""" messages = parse_all([0xf0, 0, 0xf9, 0xfd, 0, 0xf7]) assert len(messages) == 1 assert messages[0].type == 'sysex' def test_encode_and_parse_all(): """Encode and then parse all message types. This checks mostly for errors in the parser. """ parser = Parser() for type_ in sorted(specs.SPEC_BY_TYPE.keys()): msg = Message(type_) parser.feed(msg.bytes()) assert parser.get_message() == msg assert parser.get_message() is None def test_parser_ascii_text(): assert parse_all(b'7 bit ASCII should not produce any messages') == [] mido-1.3.3/tests/test_ports.py0000644000175100001770000000434514706731605017214 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2017 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT import pytest from mido.messages import Message from mido.ports import BaseIOPort class TestIOPort: class Port(BaseIOPort): def _open(self): self.close_called = False def _send(self, message): self._messages.append(message) def _close(self): self.close_called = True @pytest.fixture def port(self): with self.Port('Name') as p: yield p def test_basic(self, port): assert port.name == 'Name' assert not port.closed assert port._messages is port._parser.messages with pytest.raises(TypeError): port.send('not a message') def test_recv_non_blocking(self, port): message = Message('note_on') port.send(message) port.poll() assert port.poll() is None def test_send_message(self, port): message = Message('note_on') port.send(message) port.send(message) def test_port_close(self, port): port.close() assert port.close_called port.close_called = False port.close() assert port.closed assert not port.close_called def test_mido_port_non_blocking_recv(self, port): assert port.receive(block=False) is None def test_close_inside_iteration(): # This type of port can close when it runs out of messages. # (And example of this is socket ports.) # # Iteration should then stop after all messages in the # internal queue have been received. message = Message('note_on') class Port(BaseIOPort): def __init__(self, messages): BaseIOPort.__init__(self) # Simulate some messages that arrived # earlier. self._messages.extend(messages) self.closed = False def _receive(self, block=True): # Oops, the other end hung up. if self._messages: return self._messages.popleft() else: self.close() return None message = Message('note_on') with Port([message, message]) as port: assert len(list(port)) == 2 mido-1.3.3/tests/test_sockets.py0000644000175100001770000000262114706731605017513 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2017 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT import pytest from mido.sockets import parse_address class TestParseAddress: @pytest.mark.parametrize('input_str, expected', [(':8080', ('', 8080)), ('localhost:8080', ('localhost', 8080)) ]) def test_parse_address_normal(self, input_str, expected): assert parse_address(input_str) == expected def test_too_many_colons_raises_value_error(self): with pytest.raises(ValueError): parse_address(':to_many_colons:8080') def test_only_hostname_raises_value_error(self): with pytest.raises(ValueError): parse_address('only_hostname') def test_empty_string_raises_value_error(self): with pytest.raises(ValueError): parse_address('') def test_only_colon_raises_value_error(self): with pytest.raises(ValueError): parse_address(':') def test_non_number_port_raises_value_error(self): with pytest.raises(ValueError): parse_address(':shoe') def test_port_zero_raises_value_error(self): with pytest.raises(ValueError): parse_address(':0') def test_out_of_range_port_raises_value_error(self): with pytest.raises(ValueError): parse_address(':65536') mido-1.3.3/tests/test_syx.py0000644000175100001770000000256714706731605016674 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2017 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT from pytest import raises from mido.messages import Message from mido.syx import read_syx_file, write_syx_file def test_read(tmpdir): path = tmpdir.join("test.syx").strpath msg = Message('sysex', data=(1, 2, 3)) with open(path, 'wb') as outfile: outfile.write(msg.bin()) assert read_syx_file(path) == [msg] with open(path, 'w') as outfile: outfile.write(msg.hex()) assert read_syx_file(path) == [msg] with open(path, 'w') as outfile: outfile.write('NOT HEX') with raises(ValueError): read_syx_file(path) def test_handle_any_whitespace(tmpdir): path = tmpdir.join("test.syx").strpath with open(path, 'w') as outfile: outfile.write('F0 01 02 \t F7\n F0 03 04 F7\n') assert read_syx_file(path) == [Message('sysex', data=[1, 2]), Message('sysex', data=[3, 4])] def test_write(tmpdir): # p = tmpdir.mkdir("sub").join("hello.txt") path = tmpdir.join("test.syx").strpath msg = Message('sysex', data=(1, 2, 3)) write_syx_file(path, [msg]) with open(path, 'rb') as infile: assert infile.read() == msg.bin() write_syx_file(path, [msg], plaintext=True) with open(path) as infile: assert infile.read().strip() == msg.hex() mido-1.3.3/tests/test_tokenizer.py0000644000175100001770000000314614706731605020055 0ustar runnerdocker00000000000000# SPDX-FileCopyrightText: 2017 Ole Martin Bjorndalen # # SPDX-License-Identifier: MIT from mido.tokenizer import Tokenizer def tokenize(midi_bytes): return list(Tokenizer(midi_bytes)) def test_channel_message(): assert tokenize([0x90, 1, 2]) == [[0x90, 1, 2]] def test_sysex(): assert tokenize([0xf0, 1, 2, 3, 0xf7]) == [[0xf0, 1, 2, 3, 0xf7]] def test_empty_sysex(): assert tokenize([0xf0, 0xf7]) == [[0xf0, 0xf7]] def test_realtime(): assert tokenize([0xf8]) == [[0xf8]] def test_double_realtime(): assert tokenize([0xf8, 0xf8]) == [[0xf8], [0xf8]] def test_realtime_inside_message(): """Realtime message inside message should reset the parser.""" assert tokenize([0x90, 1, 0xf8, 2]) == [[0xf8]] def test_realtime_inside_sysex(): """Realtime messages are allowed inside sysex. The sysex messages should be delivered first. This is the only case where a message is allowed inside another message. """ assert tokenize([0xf0, 1, 0xf8, 2, 0xf7]) == [[0xf8], [0xf0, 1, 2, 0xf7]] assert tokenize([0xf0, 0xf8, 0xf7]) == [[0xf8], [0xf0, 0xf7]] def test_message_inside_sysex(): """Non-realtime message inside sysex should reset the parser.""" assert tokenize([0xf0, 0x90, 1, 2, 0xf7]) == [[0x90, 1, 2]] def test_sysex_inside_sysex(): """Sysex inside sysex should reset the parser.""" assert tokenize([0xf0, 1, 0xf0, 2, 0xf7]) == [[0xf0, 2, 0xf7]] def test_stray_data_bytes(): """Data bytes outside messages should be ignored.""" assert tokenize([0, 1, 0x90, 2, 3, 4, 5, 0xf8, 6]) == \ [[0x90, 2, 3], [0xf8]]