├── .bumpversion.cfg ├── .cookiecutterrc ├── .coveragerc ├── .editorconfig ├── .github └── workflows │ └── publish-to-test-pypi.yml ├── .gitignore ├── .travis.yml ├── AUTHORS.rst ├── CHANGELOG.rst ├── CONTRIBUTING.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── appveyor.yml ├── ci ├── appveyor-bootstrap.py ├── appveyor-download.py ├── appveyor-with-compiler.cmd ├── bootstrap.py └── templates │ ├── .travis.yml │ └── appveyor.yml ├── docs ├── authors.rst ├── changelog.rst ├── conf.py ├── contributing.rst ├── index.rst ├── installation.rst ├── readme.rst ├── reference │ ├── index.rst │ └── twelve_tone.rst ├── requirements.txt ├── spelling_wordlist.txt └── usage.rst ├── requirements.txt ├── setup.cfg ├── setup.py ├── src └── twelve_tone │ ├── __init__.py │ ├── __main__.py │ ├── cli.py │ ├── composer.py │ └── midi.py ├── tests ├── test_composer.py ├── test_midi.py └── test_twelve_tone.py └── tox.ini /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.4.2 3 | commit = True 4 | 5 | [bumpversion:file:setup.py] 6 | 7 | [bumpversion:file:docs/conf.py] 8 | 9 | [bumpversion:file:src/twelve_tone/__init__.py] 10 | -------------------------------------------------------------------------------- /.cookiecutterrc: -------------------------------------------------------------------------------- 1 | # This file exists so you can easily regenerate your project. 2 | # 3 | # `cookiepatcher` is a convenient shim around `cookiecutter` 4 | # for regenerating projects (it will generate a .cookiecutterrc 5 | # automatically for any template). To use it: 6 | # 7 | # pip install cookiepatcher 8 | # cookiepatcher gh:ionelmc/cookiecutter-pylibrary project-path 9 | # 10 | # See: 11 | # https://pypi.python.org/pypi/cookiecutter 12 | # 13 | # Alternatively, you can run: 14 | # 15 | # cookiecutter --overwrite-if-exists --config-file=project-path/.cookiecutterrc gh:ionelmc/cookiecutter-pylibrary 16 | 17 | default_context: 18 | 19 | appveyor: 'yes' 20 | c_extension_cython: 'no' 21 | c_extension_optional: 'no' 22 | c_extension_support: 'no' 23 | codacy: 'no' 24 | codeclimate: 'no' 25 | codecov: 'yes' 26 | command_line_interface: 'click' 27 | command_line_interface_bin_name: 'twelve-tone' 28 | coveralls: 'yes' 29 | distribution_name: 'twelve-tone' 30 | email: 'accraze@gmail.com' 31 | full_name: 'Andy Craze' 32 | github_username: 'accraze' 33 | landscape: 'no' 34 | package_name: 'twelve_tone' 35 | project_name: 'Twelve Tone' 36 | project_short_description: 'A Twelve-Tone matrix to generate random meelodies' 37 | release_date: 'today' 38 | repo_name: 'python-twelve-tone' 39 | requiresio: 'yes' 40 | scrutinizer: 'no' 41 | sphinx_doctest: 'no' 42 | sphinx_theme: 'sphinx-rtd-theme' 43 | test_matrix_configurator: 'no' 44 | test_matrix_separate_coverage: 'no' 45 | test_runner: 'pytest' 46 | travis: 'yes' 47 | version: '0.1.0' 48 | website: 'https://github.com/accraze' 49 | year: '2016' 50 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [paths] 2 | source = 3 | src/twelve_tone 4 | */site-packages/twelve_tone 5 | 6 | [run] 7 | branch = true 8 | source = 9 | twelve_tone 10 | tests 11 | parallel = true 12 | 13 | [report] 14 | show_missing = true 15 | precision = 2 16 | omit = *migrations* 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 4 10 | charset = utf-8 11 | 12 | [*.{bat,cmd,ps1}] 13 | end_of_line = crlf 14 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-test-pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python 🐍 distributions 📦 to PyPI and TestPyPI 2 | 3 | on: push 4 | 5 | jobs: 6 | build-n-publish: 7 | name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI 8 | runs-on: ubuntu-18.04 9 | steps: 10 | - uses: actions/checkout@master 11 | - name: Set up Python 3.7 12 | uses: actions/setup-python@v1 13 | with: 14 | python-version: 3.7 15 | - name: Install pypa/build 16 | run: >- 17 | python -m 18 | pip install 19 | build 20 | --user 21 | - name: Build a binary wheel and a source tarball 22 | run: >- 23 | python -m 24 | build 25 | --sdist 26 | --wheel 27 | --outdir dist/ 28 | - name: Publish distribution 📦 to Test PyPI 29 | uses: pypa/gh-action-pypi-publish@master 30 | with: 31 | password: ${{ secrets.TEST_PYPI_API_TOKEN }} 32 | repository_url: https://test.pypi.org/legacy/ 33 | skip_existing: true 34 | - name: Publish distribution 📦 to PyPI 35 | if: startsWith(github.ref, 'refs/tags') 36 | uses: pypa/gh-action-pypi-publish@master 37 | with: 38 | password: ${{ secrets.PYPI_API_TOKEN }} 39 | skip_existing: true 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | .eggs 13 | parts 14 | bin 15 | var 16 | sdist 17 | wheelhouse 18 | develop-eggs 19 | .installed.cfg 20 | lib 21 | lib64 22 | venv*/ 23 | pyvenv*/ 24 | 25 | # Installer logs 26 | pip-log.txt 27 | 28 | # Unit test / coverage reports 29 | .coverage 30 | .tox 31 | .coverage.* 32 | nosetests.xml 33 | coverage.xml 34 | htmlcov 35 | 36 | # Translations 37 | *.mo 38 | 39 | # Mr Developer 40 | .mr.developer.cfg 41 | .project 42 | .pydevproject 43 | .idea 44 | *.iml 45 | *.komodoproject 46 | 47 | # Complexity 48 | output/*.html 49 | output/*/index.html 50 | 51 | # Sphinx 52 | docs/_build 53 | 54 | .DS_Store 55 | *~ 56 | .*.sw[po] 57 | .build 58 | .ve 59 | .env 60 | .cache 61 | .pytest 62 | .bootstrap 63 | .appveyor.token 64 | *.bak 65 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: '3.5' 3 | dist: bionic 4 | sudo: false 5 | env: 6 | global: 7 | - LD_PRELOAD=/lib/x86_64-linux-gnu/libSegFault.so 8 | - SEGFAULT_SIGNALS=all 9 | matrix: 10 | - TOXENV=check 11 | # - TOXENV=docs 12 | 13 | - TOXENV=py27,codecov 14 | # - TOXENV=py33,codecov 15 | # - TOXENV=py34,codecov 16 | - TOXENV=py35,codecov 17 | # - TOXENV=pypy,coveralls,codecov 18 | before_install: 19 | - python --version 20 | - uname -a 21 | - lsb_release -a 22 | install: 23 | - pip install tox 24 | - virtualenv --version 25 | - easy_install --version 26 | - pip --version 27 | - tox --version 28 | - pip install -r requirements.txt 29 | script: 30 | - tox -v 31 | after_failure: 32 | - more .tox/log/* | cat 33 | - more .tox/*/log/* | cat 34 | before_cache: 35 | - rm -rf $HOME/.cache/pip/log 36 | cache: 37 | directories: 38 | - $HOME/.cache/pip 39 | notifications: 40 | email: 41 | on_success: never 42 | on_failure: always 43 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | 2 | Authors 3 | ======= 4 | 5 | * Andy Craze - https://github.com/accraze 6 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | 2 | Changelog 3 | ========= 4 | 5 | 0.4.2 (2021-03-11) 6 | ----------------------------------------- 7 | ### 0.4.1 (2021-3-11) 8 | 9 | * requirements: added missing dependency click - (#22) - @jgarte 10 | 11 | 0.4.1 (2019-12-31) 12 | ----------------------------------------- 13 | ### 0.4.1 (2019-12-31) 14 | 15 | * composer: matrix should only hold values of type 'int' (#20) 16 | 17 | 0.4.0 (2018-7-08) 18 | ----------------------------------------- 19 | 20 | * composer: added/fixed column tonerow support 21 | 22 | 0.3.0 (2018-7-04) 23 | ----------------------------------------- 24 | 25 | * cli: added random melody generator command 26 | 27 | 0.2.1 (2016-8-27) 28 | ----------------------------------------- 29 | 30 | * build: added `miditime` to setup install requirements 31 | 32 | 0.2.0 (2016-8-27) 33 | ----------------------------------------- 34 | 35 | * composer: Added save to MIDI capability 36 | 37 | 0.1.0 (2016-8-20) 38 | ----------------------------------------- 39 | 40 | * First release on PyPI. 41 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Contributing 3 | ============ 4 | 5 | Contributions are welcome, and they are greatly appreciated! Every 6 | little bit helps, and credit will always be given. 7 | 8 | Bug reports 9 | =========== 10 | 11 | When `reporting a bug `_ please include: 12 | 13 | * Your operating system name and version. 14 | * Any details about your local setup that might be helpful in troubleshooting. 15 | * Detailed steps to reproduce the bug. 16 | 17 | Documentation improvements 18 | ========================== 19 | 20 | Twelve Tone could always use more documentation, whether as part of the 21 | official Twelve Tone docs, in docstrings, or even on the web in blog posts, 22 | articles, and such. 23 | 24 | Feature requests and feedback 25 | ============================= 26 | 27 | The best way to send feedback is to file an issue at https://github.com/accraze/python-twelve-tone/issues. 28 | 29 | If you are proposing a feature: 30 | 31 | * Explain in detail how it would work. 32 | * Keep the scope as narrow as possible, to make it easier to implement. 33 | * Remember that this is a volunteer-driven project, and that code contributions are welcome :) 34 | 35 | Development 36 | =========== 37 | 38 | To set up `python-twelve-tone` for local development: 39 | 40 | 1. Fork `python-twelve-tone `_ 41 | (look for the "Fork" button). 42 | 2. Clone your fork locally:: 43 | 44 | git clone git@github.com:your_name_here/python-twelve-tone.git 45 | 46 | 3. Create a branch for local development:: 47 | 48 | git checkout -b name-of-your-bugfix-or-feature 49 | 50 | Now you can make your changes locally. 51 | 52 | 4. When you're done making changes, run all the checks, doc builder and spell checker with `tox `_ one command:: 53 | 54 | tox 55 | 56 | 5. Commit your changes and push your branch to GitHub:: 57 | 58 | git add . 59 | git commit -m "Your detailed description of your changes." 60 | git push origin name-of-your-bugfix-or-feature 61 | 62 | 6. Submit a pull request through the GitHub website. 63 | 64 | Pull Request Guidelines 65 | ----------------------- 66 | 67 | If you need some code review or feedback while you're developing the code just make the pull request. 68 | 69 | For merging, you should: 70 | 71 | 1. Include passing tests (run ``tox``) [1]_. 72 | 2. Update documentation when there's new API, functionality etc. 73 | 3. Add a note to ``CHANGELOG.rst`` about the changes. 74 | 4. Add yourself to ``AUTHORS.rst``. 75 | 76 | .. [1] If you don't have all the necessary python versions available locally you can rely on Travis - it will 77 | `run the tests `_ for each change you add in the pull request. 78 | 79 | It will be slower though ... 80 | 81 | Tips 82 | ---- 83 | 84 | To run a subset of tests:: 85 | 86 | tox -e envname -- py.test -k test_myfeature 87 | 88 | To run all the test environments in *parallel* (you need to ``pip install detox``):: 89 | 90 | detox 91 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-2021, Andy Craze 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 5 | following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following 8 | disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 11 | disclaimer in the documentation and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 14 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 16 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 17 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 18 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 19 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft docs 2 | graft examples 3 | graft src 4 | graft ci 5 | graft tests 6 | 7 | include .bumpversion.cfg 8 | include .coveragerc 9 | include .cookiecutterrc 10 | include .editorconfig 11 | include .isort.cfg 12 | 13 | include AUTHORS.rst 14 | include CHANGELOG.rst 15 | include CONTRIBUTING.rst 16 | include LICENSE 17 | include README.rst 18 | include requirements.txt 19 | 20 | include tox.ini .travis.yml appveyor.yml 21 | 22 | global-exclude *.py[cod] __pycache__ *.so *.dylib 23 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Overview 3 | ======== 4 | 5 | .. start-badges 6 | 7 | .. list-table:: 8 | :stub-columns: 1 9 | 10 | * - docs 11 | - |docs| 12 | * - tests 13 | - |travis| |codecov| 14 | * - package 15 | - |version| |wheel| |supported-versions| 16 | 17 | .. |docs| image:: https://readthedocs.org/projects/python-twelve-tone/badge/?style=flat 18 | :target: https://readthedocs.org/projects/python-twelve-tone 19 | :alt: Documentation Status 20 | 21 | .. |travis| image:: https://travis-ci.org/accraze/python-twelve-tone.svg?branch=master 22 | :alt: Travis-CI Build Status 23 | :target: https://travis-ci.org/accraze/python-twelve-tone 24 | 25 | .. |codecov| image:: https://codecov.io/github/accraze/python-twelve-tone/coverage.svg?branch=master 26 | :alt: Coverage Status 27 | :target: https://codecov.io/github/accraze/python-twelve-tone 28 | 29 | .. |version| image:: https://img.shields.io/pypi/v/twelve-tone.svg?style=flat 30 | :alt: PyPI Package latest release 31 | :target: https://pypi.python.org/pypi/twelve-tone 32 | 33 | 34 | .. |wheel| image:: https://img.shields.io/pypi/wheel/twelve-tone.svg?style=flat 35 | :alt: PyPI Wheel 36 | :target: https://pypi.python.org/pypi/twelve-tone 37 | 38 | .. |supported-versions| image:: https://img.shields.io/pypi/pyversions/twelve-tone.svg?style=flat 39 | :alt: Supported versions 40 | :target: https://pypi.python.org/pypi/twelve-tone 41 | 42 | 43 | 44 | .. end-badges 45 | 46 | Twelve-tone matrix to generate dodecaphonic melodies. 47 | 48 | 49 | .. image:: https://upload.wikimedia.org/wikipedia/commons/thumb/7/77/Schoenberg_-_Piano_Piece_op.33a_tone_row.png/640px-Schoenberg_-_Piano_Piece_op.33a_tone_row.png 50 | 51 | Following a process created by the composer Arnold Schoenberg, this library 52 | computes a matrix to create twelve-tone serialism melodies which compose each 53 | of the 12 semitones of the chromatic scale with equal importance. 54 | 55 | * Save your compositions to MIDI 56 | * Free software: BSD license 57 | 58 | Installation 59 | ============ 60 | 61 | :: 62 | 63 | pip install twelve-tone 64 | 65 | The `GuixRUs `_ channel also provides ``twelve-tone``. 66 | 67 | Quick Start 68 | =========== 69 | 70 | You can quickly generate a random twelve-tone melody with the CLI 71 | 72 | :: 73 | 74 | $ twelve-tone 75 | ['C# / Db', 'A# / Bb', 'F', 'D', 'G# / Ab', 'D# / Eb', 'F# / Gb', 76 | 'A', 'C', 'G', 'B', 'E'] 77 | 78 | Or you can use the following methods in a script: 79 | 80 | :: 81 | 82 | >>> from twelve_tone.composer import Composer 83 | >>> c = Composer() 84 | >>> c.compose() 85 | >>> c.get_melody() 86 | ['C# / Db', 'A# / Bb', 'F', 'D', 'G# / Ab', 'D# / Eb', 'F# / Gb', 87 | 'A', 'C', 'G', 'B', 'E'] 88 | 89 | After you have composed a matrix of tone rows, you can save the composition to 90 | MIDI: 91 | 92 | :: 93 | 94 | >>> c.compose() 95 | >>> c.save_to_midi(filename='TWELVE_TONE.mid') 96 | 97 | The new MIDI file will be created in your current working directory. If you do 98 | not specify a `filename` for your file, it will default to `example.mid`. 99 | 100 | Documentation 101 | ============= 102 | 103 | https://python-twelve-tone.readthedocs.io/ 104 | 105 | Development 106 | =========== 107 | 108 | To run the all tests run:: 109 | 110 | tox 111 | 112 | Note, to combine the coverage data from all the tox environments run: 113 | 114 | .. list-table:: 115 | :widths: 10 90 116 | :stub-columns: 1 117 | 118 | - - Windows 119 | - :: 120 | 121 | set PYTEST_ADDOPTS=--cov-append 122 | tox 123 | 124 | - - Other 125 | - :: 126 | 127 | PYTEST_ADDOPTS=--cov-append tox 128 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{branch}-{build}' 2 | build: off 3 | cache: 4 | - '%LOCALAPPDATA%\pip\Cache' 5 | environment: 6 | global: 7 | WITH_COMPILER: 'cmd /E:ON /V:ON /C .\ci\appveyor-with-compiler.cmd' 8 | matrix: 9 | - TOXENV: check 10 | PYTHON_HOME: C:\Python27 11 | PYTHON_VERSION: '2.7' 12 | PYTHON_ARCH: '32' 13 | 14 | - TOXENV: 'py27,codecov' 15 | TOXPYTHON: C:\Python27\python.exe 16 | PYTHON_HOME: C:\Python27 17 | PYTHON_VERSION: '2.7' 18 | PYTHON_ARCH: '32' 19 | 20 | - TOXENV: 'py27,codecov' 21 | TOXPYTHON: C:\Python27-x64\python.exe 22 | WINDOWS_SDK_VERSION: v7.0 23 | PYTHON_HOME: C:\Python27-x64 24 | PYTHON_VERSION: '2.7' 25 | PYTHON_ARCH: '64' 26 | 27 | - TOXENV: 'py34,codecov' 28 | TOXPYTHON: C:\Python34\python.exe 29 | PYTHON_HOME: C:\Python34 30 | PYTHON_VERSION: '3.4' 31 | PYTHON_ARCH: '32' 32 | 33 | - TOXENV: 'py34,codecov' 34 | TOXPYTHON: C:\Python34-x64\python.exe 35 | WINDOWS_SDK_VERSION: v7.1 36 | PYTHON_HOME: C:\Python34-x64 37 | PYTHON_VERSION: '3.4' 38 | PYTHON_ARCH: '64' 39 | 40 | - TOXENV: 'py35,codecov' 41 | TOXPYTHON: C:\Python35\python.exe 42 | PYTHON_HOME: C:\Python35 43 | PYTHON_VERSION: '3.5' 44 | PYTHON_ARCH: '32' 45 | 46 | - TOXENV: 'py35,codecov' 47 | TOXPYTHON: C:\Python35-x64\python.exe 48 | PYTHON_HOME: C:\Python35-x64 49 | PYTHON_VERSION: '3.5' 50 | PYTHON_ARCH: '64' 51 | 52 | init: 53 | - ps: echo $env:TOXENV 54 | - ps: ls C:\Python* 55 | install: 56 | - python -u ci\appveyor-bootstrap.py 57 | - '%PYTHON_HOME%\Scripts\virtualenv --version' 58 | - '%PYTHON_HOME%\Scripts\easy_install --version' 59 | - '%PYTHON_HOME%\Scripts\pip --version' 60 | - '%PYTHON_HOME%\Scripts\tox --version' 61 | test_script: 62 | - '%WITH_COMPILER% %PYTHON_HOME%\Scripts\tox' 63 | 64 | on_failure: 65 | - ps: dir "env:" 66 | - ps: get-content .tox\*\log\* 67 | artifacts: 68 | - path: dist\* 69 | 70 | ### To enable remote debugging uncomment this (also, see: http://www.appveyor.com/docs/how-to/rdp-to-build-worker): 71 | # on_finish: 72 | # - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) 73 | -------------------------------------------------------------------------------- /ci/appveyor-bootstrap.py: -------------------------------------------------------------------------------- 1 | """ 2 | AppVeyor will at least have few Pythons around so there's no point of implementing a bootstrapper in PowerShell. 3 | 4 | This is a port of https://github.com/pypa/python-packaging-user-guide/blob/master/source/code/install.ps1 5 | with various fixes and improvements that just weren't feasible to implement in PowerShell. 6 | """ 7 | from __future__ import print_function 8 | 9 | from os import environ 10 | from os.path import exists 11 | from subprocess import CalledProcessError 12 | from subprocess import check_call 13 | 14 | try: 15 | from urllib.request import urlretrieve 16 | except ImportError: 17 | from urllib import urlretrieve 18 | 19 | BASE_URL = "https://www.python.org/ftp/python/" 20 | GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py" 21 | GET_PIP_PATH = "C:\get-pip.py" 22 | URLS = { 23 | ("2.7", "64"): BASE_URL + "2.7.10/python-2.7.10.amd64.msi", 24 | ("2.7", "32"): BASE_URL + "2.7.10/python-2.7.10.msi", 25 | # NOTE: no .msi installer for 3.3.6 26 | ("3.3", "64"): BASE_URL + "3.3.3/python-3.3.3.amd64.msi", 27 | ("3.3", "32"): BASE_URL + "3.3.3/python-3.3.3.msi", 28 | ("3.4", "64"): BASE_URL + "3.4.3/python-3.4.3.amd64.msi", 29 | ("3.4", "32"): BASE_URL + "3.4.3/python-3.4.3.msi", 30 | ("3.5", "64"): BASE_URL + "3.5.0/python-3.5.0-amd64.exe", 31 | ("3.5", "32"): BASE_URL + "3.5.0/python-3.5.0.exe", 32 | } 33 | INSTALL_CMD = { 34 | # Commands are allowed to fail only if they are not the last command. Eg: uninstall (/x) allowed to fail. 35 | "2.7": [["msiexec.exe", "/L*+!", "install.log", "/qn", "/x", "{path}"], 36 | ["msiexec.exe", "/L*+!", "install.log", "/qn", "/i", "{path}", "TARGETDIR={home}"]], 37 | "3.3": [["msiexec.exe", "/L*+!", "install.log", "/qn", "/x", "{path}"], 38 | ["msiexec.exe", "/L*+!", "install.log", "/qn", "/i", "{path}", "TARGETDIR={home}"]], 39 | "3.4": [["msiexec.exe", "/L*+!", "install.log", "/qn", "/x", "{path}"], 40 | ["msiexec.exe", "/L*+!", "install.log", "/qn", "/i", "{path}", "TARGETDIR={home}"]], 41 | "3.5": [["{path}", "/quiet", "TargetDir={home}"]], 42 | } 43 | 44 | 45 | def download_file(url, path): 46 | print("Downloading: {} (into {})".format(url, path)) 47 | progress = [0, 0] 48 | 49 | def report(count, size, total): 50 | progress[0] = count * size 51 | if progress[0] - progress[1] > 1000000: 52 | progress[1] = progress[0] 53 | print("Downloaded {:,}/{:,} ...".format(progress[1], total)) 54 | 55 | dest, _ = urlretrieve(url, path, reporthook=report) 56 | return dest 57 | 58 | 59 | def install_python(version, arch, home): 60 | print("Installing Python", version, "for", arch, "bit architecture to", home) 61 | if exists(home): 62 | return 63 | 64 | path = download_python(version, arch) 65 | print("Installing", path, "to", home) 66 | success = False 67 | for cmd in INSTALL_CMD[version]: 68 | cmd = [part.format(home=home, path=path) for part in cmd] 69 | print("Running:", " ".join(cmd)) 70 | try: 71 | check_call(cmd) 72 | except CalledProcessError as exc: 73 | print("Failed command", cmd, "with:", exc) 74 | if exists("install.log"): 75 | with open("install.log") as fh: 76 | print(fh.read()) 77 | else: 78 | success = True 79 | if success: 80 | print("Installation complete!") 81 | else: 82 | print("Installation failed") 83 | 84 | 85 | def download_python(version, arch): 86 | for _ in range(3): 87 | try: 88 | return download_file(URLS[version, arch], "installer.exe") 89 | except Exception as exc: 90 | print("Failed to download:", exc) 91 | print("Retrying ...") 92 | 93 | 94 | def install_pip(home): 95 | pip_path = home + "/Scripts/pip.exe" 96 | python_path = home + "/python.exe" 97 | if exists(pip_path): 98 | print("pip already installed.") 99 | else: 100 | print("Installing pip...") 101 | download_file(GET_PIP_URL, GET_PIP_PATH) 102 | print("Executing:", python_path, GET_PIP_PATH) 103 | check_call([python_path, GET_PIP_PATH]) 104 | 105 | 106 | def install_packages(home, *packages): 107 | cmd = [home + "/Scripts/pip.exe", "install"] 108 | cmd.extend(packages) 109 | check_call(cmd) 110 | 111 | 112 | if __name__ == "__main__": 113 | install_python(environ['PYTHON_VERSION'], environ['PYTHON_ARCH'], environ['PYTHON_HOME']) 114 | install_pip(environ['PYTHON_HOME']) 115 | install_packages(environ['PYTHON_HOME'], "setuptools>=18.0.1", "wheel", "tox", "virtualenv>=13.1.0") 116 | -------------------------------------------------------------------------------- /ci/appveyor-download.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Use the AppVeyor API to download Windows artifacts. 4 | 5 | Taken from: https://bitbucket.org/ned/coveragepy/src/tip/ci/download_appveyor.py 6 | # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 7 | # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt 8 | """ 9 | from __future__ import unicode_literals 10 | 11 | import argparse 12 | import os 13 | import zipfile 14 | 15 | import requests 16 | 17 | 18 | def make_auth_headers(): 19 | """Make the authentication headers needed to use the Appveyor API.""" 20 | path = os.path.expanduser("~/.appveyor.token") 21 | if not os.path.exists(path): 22 | raise RuntimeError( 23 | "Please create a file named `.appveyor.token` in your home directory. " 24 | "You can get the token from https://ci.appveyor.com/api-token" 25 | ) 26 | with open(path) as f: 27 | token = f.read().strip() 28 | 29 | headers = { 30 | 'Authorization': 'Bearer {}'.format(token), 31 | } 32 | return headers 33 | 34 | 35 | def download_latest_artifacts(account_project, build_id): 36 | """Download all the artifacts from the latest build.""" 37 | if build_id is None: 38 | url = "https://ci.appveyor.com/api/projects/{}".format(account_project) 39 | else: 40 | url = "https://ci.appveyor.com/api/projects/{}/build/{}".format(account_project, build_id) 41 | build = requests.get(url, headers=make_auth_headers()).json() 42 | jobs = build['build']['jobs'] 43 | print(u"Build {0[build][version]}, {1} jobs: {0[build][message]}".format(build, len(jobs))) 44 | 45 | for job in jobs: 46 | name = job['name'] 47 | print(u" {0}: {1[status]}, {1[artifactsCount]} artifacts".format(name, job)) 48 | 49 | url = "https://ci.appveyor.com/api/buildjobs/{}/artifacts".format(job['jobId']) 50 | response = requests.get(url, headers=make_auth_headers()) 51 | artifacts = response.json() 52 | 53 | for artifact in artifacts: 54 | is_zip = artifact['type'] == "Zip" 55 | filename = artifact['fileName'] 56 | print(u" {0}, {1} bytes".format(filename, artifact['size'])) 57 | 58 | url = "https://ci.appveyor.com/api/buildjobs/{}/artifacts/{}".format(job['jobId'], filename) 59 | download_url(url, filename, make_auth_headers()) 60 | 61 | if is_zip: 62 | unpack_zipfile(filename) 63 | os.remove(filename) 64 | 65 | 66 | def ensure_dirs(filename): 67 | """Make sure the directories exist for `filename`.""" 68 | dirname = os.path.dirname(filename) 69 | if dirname and not os.path.exists(dirname): 70 | os.makedirs(dirname) 71 | 72 | 73 | def download_url(url, filename, headers): 74 | """Download a file from `url` to `filename`.""" 75 | ensure_dirs(filename) 76 | response = requests.get(url, headers=headers, stream=True) 77 | if response.status_code == 200: 78 | with open(filename, 'wb') as f: 79 | for chunk in response.iter_content(16 * 1024): 80 | f.write(chunk) 81 | else: 82 | print(u" Error downloading {}: {}".format(url, response)) 83 | 84 | 85 | def unpack_zipfile(filename): 86 | """Unpack a zipfile, using the names in the zip.""" 87 | with open(filename, 'rb') as fzip: 88 | z = zipfile.ZipFile(fzip) 89 | for name in z.namelist(): 90 | print(u" extracting {}".format(name)) 91 | ensure_dirs(name) 92 | z.extract(name) 93 | 94 | 95 | parser = argparse.ArgumentParser(description='Download artifacts from AppVeyor.') 96 | parser.add_argument('--id', 97 | metavar='PROJECT_ID', 98 | default='accraze/python-twelve-tone', 99 | help='Project ID in AppVeyor.') 100 | parser.add_argument('build', 101 | nargs='?', 102 | metavar='BUILD_ID', 103 | help='Build ID in AppVeyor. Eg: master-123') 104 | 105 | if __name__ == "__main__": 106 | # import logging 107 | # logging.basicConfig(level="DEBUG") 108 | args = parser.parse_args() 109 | download_latest_artifacts(args.id, args.build) 110 | -------------------------------------------------------------------------------- /ci/appveyor-with-compiler.cmd: -------------------------------------------------------------------------------- 1 | :: To build extensions for 64 bit Python 3, we need to configure environment 2 | :: variables to use the MSVC 2010 C++ compilers from GRMSDKX_EN_DVD.iso of: 3 | :: MS Windows SDK for Windows 7 and .NET Framework 4 (SDK v7.1) 4 | :: 5 | :: To build extensions for 64 bit Python 2, we need to configure environment 6 | :: variables to use the MSVC 2008 C++ compilers from GRMSDKX_EN_DVD.iso of: 7 | :: MS Windows SDK for Windows 7 and .NET Framework 3.5 (SDK v7.0) 8 | :: 9 | :: 32 bit builds do not require specific environment configurations. 10 | :: 11 | :: Note: this script needs to be run with the /E:ON and /V:ON flags for the 12 | :: cmd interpreter, at least for (SDK v7.0) 13 | :: 14 | :: More details at: 15 | :: https://github.com/cython/cython/wiki/64BitCythonExtensionsOnWindows 16 | :: http://stackoverflow.com/a/13751649/163740 17 | :: 18 | :: Author: Olivier Grisel 19 | :: License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/ 20 | SET COMMAND_TO_RUN=%* 21 | SET WIN_SDK_ROOT=C:\Program Files\Microsoft SDKs\Windows 22 | SET WIN_WDK="c:\Program Files (x86)\Windows Kits\10\Include\wdf" 23 | ECHO SDK: %WINDOWS_SDK_VERSION% ARCH: %PYTHON_ARCH% 24 | 25 | 26 | IF "%PYTHON_VERSION%"=="3.5" ( 27 | IF EXIST %WIN_WDK% ( 28 | REM See: https://connect.microsoft.com/VisualStudio/feedback/details/1610302/ 29 | REN %WIN_WDK% 0wdf 30 | ) 31 | GOTO main 32 | ) 33 | 34 | IF "%PYTHON_ARCH%"=="32" ( 35 | GOTO main 36 | ) 37 | 38 | SET DISTUTILS_USE_SDK=1 39 | SET MSSdk=1 40 | "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Setup\WindowsSdkVer.exe" -q -version:%WINDOWS_SDK_VERSION% 41 | CALL "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Bin\SetEnv.cmd" /x64 /release 42 | 43 | :main 44 | 45 | ECHO Executing: %COMMAND_TO_RUN% 46 | CALL %COMMAND_TO_RUN% || EXIT 1 47 | -------------------------------------------------------------------------------- /ci/bootstrap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import absolute_import, print_function, unicode_literals 4 | 5 | import os 6 | import sys 7 | from os.path import abspath 8 | from os.path import dirname 9 | from os.path import exists 10 | from os.path import join 11 | 12 | 13 | if __name__ == "__main__": 14 | base_path = dirname(dirname(abspath(__file__))) 15 | print("Project path: {0}".format(base_path)) 16 | env_path = join(base_path, ".tox", "bootstrap") 17 | if sys.platform == "win32": 18 | bin_path = join(env_path, "Scripts") 19 | else: 20 | bin_path = join(env_path, "bin") 21 | if not exists(env_path): 22 | import subprocess 23 | 24 | print("Making bootstrap env in: {0} ...".format(env_path)) 25 | try: 26 | subprocess.check_call(["virtualenv", env_path]) 27 | except subprocess.CalledProcessError: 28 | subprocess.check_call([sys.executable, "-m", "virtualenv", env_path]) 29 | print("Installing `jinja2` into bootstrap environment...") 30 | subprocess.check_call([join(bin_path, "pip"), "install", "jinja2"]) 31 | activate = join(bin_path, "activate_this.py") 32 | # noinspection PyCompatibility 33 | exec(compile(open(activate, "rb").read(), activate, "exec"), dict(__file__=activate)) 34 | 35 | import jinja2 36 | 37 | import subprocess 38 | 39 | jinja = jinja2.Environment( 40 | loader=jinja2.FileSystemLoader(join(base_path, "ci", "templates")), 41 | trim_blocks=True, 42 | lstrip_blocks=True, 43 | keep_trailing_newline=True 44 | ) 45 | 46 | tox_environments = [ 47 | line.strip() 48 | # WARNING: 'tox' must be installed globally or in the project's virtualenv 49 | for line in subprocess.check_output(['tox', '--listenvs'], universal_newlines=True).splitlines() 50 | ] 51 | tox_environments = [line for line in tox_environments if line not in ['clean', 'report', 'docs', 'check']] 52 | 53 | for name in os.listdir(join("ci", "templates")): 54 | with open(join(base_path, name), "w") as fh: 55 | fh.write(jinja.get_template(name).render(tox_environments=tox_environments)) 56 | print("Wrote {}".format(name)) 57 | print("DONE.") 58 | -------------------------------------------------------------------------------- /ci/templates/.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: '3.5' 3 | sudo: false 4 | env: 5 | global: 6 | - LD_PRELOAD=/lib/x86_64-linux-gnu/libSegFault.so 7 | - SEGFAULT_SIGNALS=all 8 | matrix: 9 | - TOXENV=check 10 | - TOXENV=docs 11 | {% for env in tox_environments %}{{ '' }} 12 | - TOXENV={{ env }},coveralls,codecov 13 | {% endfor %} 14 | 15 | before_install: 16 | - python --version 17 | - uname -a 18 | - lsb_release -a 19 | install: 20 | - pip install tox 21 | - virtualenv --version 22 | - easy_install --version 23 | - pip --version 24 | - tox --version 25 | script: 26 | - tox -v 27 | after_failure: 28 | - more .tox/log/* | cat 29 | - more .tox/*/log/* | cat 30 | before_cache: 31 | - rm -rf $HOME/.cache/pip/log 32 | cache: 33 | directories: 34 | - $HOME/.cache/pip 35 | notifications: 36 | email: 37 | on_success: never 38 | on_failure: always 39 | -------------------------------------------------------------------------------- /ci/templates/appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{branch}-{build}' 2 | build: off 3 | cache: 4 | - '%LOCALAPPDATA%\pip\Cache' 5 | environment: 6 | global: 7 | WITH_COMPILER: 'cmd /E:ON /V:ON /C .\ci\appveyor-with-compiler.cmd' 8 | matrix: 9 | - TOXENV: check 10 | PYTHON_HOME: C:\Python27 11 | PYTHON_VERSION: '2.7' 12 | PYTHON_ARCH: '32' 13 | 14 | {% for env in tox_environments %}{% if env.startswith(('py27', 'py34', 'py35')) %} 15 | - TOXENV: '{{ env }},codecov' 16 | TOXPYTHON: C:\Python{{ env[2:4] }}\python.exe 17 | PYTHON_HOME: C:\Python{{ env[2:4] }} 18 | PYTHON_VERSION: '{{ env[2] }}.{{ env[3] }}' 19 | PYTHON_ARCH: '32' 20 | 21 | - TOXENV: '{{ env }},codecov' 22 | TOXPYTHON: C:\Python{{ env[2:4] }}-x64\python.exe 23 | {%- if env.startswith(('py2', 'py33', 'py34')) %} 24 | 25 | WINDOWS_SDK_VERSION: v7.{{ '1' if env.startswith('py3') else '0' }} 26 | {%- endif %} 27 | 28 | PYTHON_HOME: C:\Python{{ env[2:4] }}-x64 29 | PYTHON_VERSION: '{{ env[2] }}.{{ env[3] }}' 30 | PYTHON_ARCH: '64' 31 | 32 | {% endif %}{% endfor %} 33 | init: 34 | - ps: echo $env:TOXENV 35 | - ps: ls C:\Python* 36 | install: 37 | - python -u ci\appveyor-bootstrap.py 38 | - '%PYTHON_HOME%\Scripts\virtualenv --version' 39 | - '%PYTHON_HOME%\Scripts\easy_install --version' 40 | - '%PYTHON_HOME%\Scripts\pip --version' 41 | - '%PYTHON_HOME%\Scripts\tox --version' 42 | test_script: 43 | - '%WITH_COMPILER% %PYTHON_HOME%\Scripts\tox' 44 | 45 | on_failure: 46 | - ps: dir "env:" 47 | - ps: get-content .tox\*\log\* 48 | artifacts: 49 | - path: dist\* 50 | 51 | ### To enable remote debugging uncomment this (also, see: http://www.appveyor.com/docs/how-to/rdp-to-build-worker): 52 | # on_finish: 53 | # - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) 54 | -------------------------------------------------------------------------------- /docs/authors.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../AUTHORS.rst 2 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGELOG.rst 2 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import os 5 | 6 | 7 | extensions = [ 8 | 'sphinx.ext.autodoc', 9 | 'sphinx.ext.autosummary', 10 | 'sphinx.ext.coverage', 11 | 'sphinx.ext.doctest', 12 | 'sphinx.ext.extlinks', 13 | 'sphinx.ext.ifconfig', 14 | 'sphinx.ext.napoleon', 15 | 'sphinx.ext.todo', 16 | 'sphinx.ext.viewcode', 17 | ] 18 | if os.getenv('SPELLCHECK'): 19 | extensions += 'sphinxcontrib.spelling', 20 | spelling_show_suggestions = True 21 | spelling_lang = 'en_US' 22 | 23 | source_suffix = '.rst' 24 | master_doc = 'index' 25 | project = u'Twelve Tone' 26 | year = u'2018' 27 | author = u'Andy Craze' 28 | copyright = '{0}, {1}'.format(year, author) 29 | version = release = u'0.4.2' 30 | 31 | pygments_style = 'trac' 32 | templates_path = ['.'] 33 | extlinks = { 34 | 'issue': ('https://github.com/accraze/python-twelve-tone/issues/%s', '#'), 35 | 'pr': ('https://github.com/accraze/python-twelve-tone/pull/%s', 'PR #'), 36 | } 37 | # on_rtd is whether we are on readthedocs.org 38 | on_rtd = os.environ.get('READTHEDOCS', None) == 'True' 39 | 40 | if not on_rtd: # only set the theme if we're building docs locally 41 | html_theme = 'sphinx_rtd_theme' 42 | 43 | html_use_smartypants = True 44 | html_last_updated_fmt = '%b %d, %Y' 45 | html_split_index = False 46 | html_sidebars = { 47 | '**': ['searchbox.html', 'globaltoc.html', 'sourcelink.html'], 48 | } 49 | html_short_title = '%s-%s' % (project, version) 50 | 51 | napoleon_use_ivar = True 52 | napoleon_use_rtype = False 53 | napoleon_use_param = False 54 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTING.rst 2 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Contents 3 | ======== 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | 8 | readme 9 | installation 10 | usage 11 | reference/index 12 | contributing 13 | authors 14 | changelog 15 | 16 | Indices and tables 17 | ================== 18 | 19 | * :ref:`genindex` 20 | * :ref:`modindex` 21 | * :ref:`search` 22 | 23 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Installation 3 | ============ 4 | 5 | At the command line:: 6 | 7 | pip install twelve-tone 8 | -------------------------------------------------------------------------------- /docs/readme.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | -------------------------------------------------------------------------------- /docs/reference/index.rst: -------------------------------------------------------------------------------- 1 | Reference 2 | ========= 3 | 4 | .. toctree:: 5 | :glob: 6 | 7 | twelve_tone* 8 | -------------------------------------------------------------------------------- /docs/reference/twelve_tone.rst: -------------------------------------------------------------------------------- 1 | twelve_tone 2 | =========== 3 | 4 | .. testsetup:: 5 | 6 | from twelve_tone import * 7 | 8 | .. automodule:: twelve_tone 9 | :members: 10 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx>=1.3 2 | sphinx-rtd-theme 3 | -e . 4 | -------------------------------------------------------------------------------- /docs/spelling_wordlist.txt: -------------------------------------------------------------------------------- 1 | builtin 2 | builtins 3 | classmethod 4 | staticmethod 5 | classmethods 6 | staticmethods 7 | args 8 | kwargs 9 | callstack 10 | Changelog 11 | Indices 12 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | Usage 3 | ===== 4 | 5 | To use Twelve Tone in a project:: 6 | 7 | import twelve_tone 8 | 9 | You can quickly generate a random twelve-tone melody with the CLI 10 | 11 | :: 12 | 13 | $ twelve-tone 14 | ['C# / Db', 'A# / Bb', 'F', 'D', 'G# / Ab', 'D# / Eb', 'F# / Gb', 15 | 'A', 'C', 'G', 'B', 'E'] 16 | 17 | Or you can use the following methods in a script: 18 | 19 | :: 20 | 21 | >>> from twelve_tone.composer import Composer 22 | >>> c = Composer() 23 | >>> c.compose() 24 | >>> c.get_melody() 25 | ['C# / Db', 'A# / Bb', 'F', 'D', 'G# / Ab', 'D# / Eb', 'F# / Gb', 26 | 'A', 'C', 'G', 'B', 'E'] 27 | 28 | After you have composed a matrix of tone rows, you can save the composition to 29 | MIDI: 30 | 31 | :: 32 | 33 | >>> c.compose() 34 | >>> c.save_to_midi(filename='TWELVE_TONE.mid') 35 | 36 | 37 | You can even select either a specific row or column when generating a melody. 38 | 39 | :: 40 | 41 | from twelve_tone.composer import Composer 42 | c = Composer() 43 | c.compose() 44 | melody_row = c.get_melody(row=3) 45 | melody_col = c.get_melody(column=7) 46 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | miditime 3 | click 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [flake8] 5 | max-line-length = 140 6 | exclude = tests/*,*/migrations/*,*/south_migrations/* 7 | 8 | [tool:pytest] 9 | norecursedirs = 10 | .git 11 | .tox 12 | .env 13 | dist 14 | build 15 | south_migrations 16 | migrations 17 | python_files = 18 | test_*.py 19 | *_test.py 20 | tests.py 21 | addopts = 22 | -rxEfsw 23 | --strict 24 | --doctest-modules 25 | --doctest-glob=\*.rst 26 | --tb=short 27 | 28 | [isort] 29 | force_single_line=True 30 | line_length=120 31 | known_first_party=twelve_tone 32 | default_section=THIRDPARTY 33 | forced_separate=test_twelve_tone 34 | not_skip = __init__.py 35 | skip = migrations, south_migrations 36 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | from __future__ import absolute_import 4 | from __future__ import print_function 5 | 6 | import io 7 | import re 8 | from glob import glob 9 | from os.path import basename 10 | from os.path import dirname 11 | from os.path import join 12 | from os.path import splitext 13 | 14 | from setuptools import find_packages 15 | from setuptools import setup 16 | 17 | 18 | def read(*names, **kwargs): 19 | return io.open( 20 | join(dirname(__file__), *names), 21 | encoding=kwargs.get('encoding', 'utf8') 22 | ).read() 23 | 24 | 25 | setup( 26 | name='twelve-tone', 27 | version='0.4.2', 28 | license='BSD', 29 | description='Twelve-tone matrix to generate dodecaphonic melodies', 30 | long_description='%s\n%s' % ( 31 | re.compile('^.. start-badges.*^.. end-badges', re.M | re.S).sub('', read('README.rst')), 32 | re.sub(':[a-z]+:`~?(.*?)`', r'``\1``', read('CHANGELOG.rst')) 33 | ), 34 | author='Andy Craze', 35 | author_email='accraze@gmail.com', 36 | url='https://github.com/accraze/python-twelve-tone', 37 | packages=find_packages('src'), 38 | package_dir={'': 'src'}, 39 | py_modules=[splitext(basename(path))[0] for path in glob('src/*.py')], 40 | include_package_data=True, 41 | zip_safe=False, 42 | classifiers=[ 43 | # complete classifier list: http://pypi.python.org/pypi?%3Aaction=list_classifiers 44 | 'Development Status :: 5 - Production/Stable', 45 | 'Intended Audience :: Developers', 46 | 'License :: OSI Approved :: BSD License', 47 | 'Operating System :: Unix', 48 | 'Operating System :: POSIX', 49 | 'Operating System :: Microsoft :: Windows', 50 | 'Programming Language :: Python', 51 | 'Programming Language :: Python :: 2.7', 52 | 'Programming Language :: Python :: 3', 53 | 'Programming Language :: Python :: 3.3', 54 | 'Programming Language :: Python :: 3.4', 55 | 'Programming Language :: Python :: 3.5', 56 | 'Topic :: Utilities', 57 | ], 58 | keywords=[ 59 | 'music', 'composition', 'matrix', 'atonal', 'midi' 60 | ], 61 | install_requires=[ 62 | 'click', 63 | 'numpy', 64 | 'miditime' 65 | ], 66 | extras_require={ 67 | # eg: 68 | # 'rst': ['docutils>=0.11'], 69 | # ':python_version=="2.6"': ['argparse'], 70 | }, 71 | entry_points={ 72 | 'console_scripts': [ 73 | 'twelve-tone = twelve_tone.cli:main', 74 | ] 75 | }, 76 | ) 77 | -------------------------------------------------------------------------------- /src/twelve_tone/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.4.2" 2 | -------------------------------------------------------------------------------- /src/twelve_tone/__main__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Entrypoint module, in case you use `python -mtwelve_tone`. 3 | 4 | 5 | Why does this file exist, and why __main__? For more info, read: 6 | 7 | - https://www.python.org/dev/peps/pep-0338/ 8 | - https://docs.python.org/2/using/cmdline.html#cmdoption-m 9 | - https://docs.python.org/3/using/cmdline.html#cmdoption-m 10 | """ 11 | from twelve_tone.cli import main 12 | 13 | if __name__ == "__main__": 14 | main() 15 | -------------------------------------------------------------------------------- /src/twelve_tone/cli.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module that contains the command line app. 3 | 4 | Why does this file exist, and why not put this in __main__? 5 | 6 | You might be tempted to import things from __main__ later, but that will cause 7 | problems: the code will get executed twice: 8 | 9 | - When you run `python -mtwelve_tone` python will execute 10 | ``__main__.py`` as a script. That means there won't be any 11 | ``twelve_tone.__main__`` in ``sys.modules``. 12 | - When you import __main__ it will get executed again (as a module) because 13 | there's no ``twelve_tone.__main__`` in ``sys.modules``. 14 | 15 | Also see (1) from http://click.pocoo.org/5/setuptools/#setuptools-integration 16 | """ 17 | import click 18 | 19 | from twelve_tone.composer import Composer 20 | 21 | 22 | @click.command() 23 | @click.option('--row', '-r', default=0, help='Row to use as row tone') 24 | @click.option('--column', '-c', default=0, help='Column to use as column tone') 25 | @click.option('--midi', '-m', help='MIDI output file') 26 | def main(row, column, midi): 27 | c = Composer() 28 | c.compose() 29 | if row < 0 or column < 0: 30 | click.echo("Invalid row or column arguments.") 31 | exit(1) 32 | elif row >= c.matrix.shape[0]: 33 | click.echo("Row number exceeds melody row count.") 34 | exit(1) 35 | elif column >= c.matrix.shape[1]: 36 | click.echo("Column number exceeds melody column count.") 37 | exit(1) 38 | click.echo(c.get_melody(row=row, column=column)) 39 | if midi is not None: 40 | c.save_to_midi(filename=midi) 41 | -------------------------------------------------------------------------------- /src/twelve_tone/composer.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | import numpy as np 4 | 5 | from .midi import MIDIFile # noqa 6 | 7 | 8 | class Composer(object): 9 | matrix = np.zeros((12, 12), dtype=int) 10 | 11 | def compose(self, top_row=None): 12 | # top_row 13 | self._load_top_row(top_row) 14 | # load first column 15 | self._load_first_column() 16 | # load rest of matrix 17 | self._compute_matrix() 18 | 19 | return self.matrix 20 | 21 | def get_melody(self, row=0, column=None): 22 | """ 23 | Returns a tone row that can be used 24 | as a 12 tone melody. 25 | 26 | You can specify a specific row or column, 27 | otherwise the top most tone row will be returned. 28 | """ 29 | melody = [] 30 | tone_row = self._get_tone_row(row, column) 31 | 32 | for cell in tone_row: 33 | melody.append(self.get_pitch(int(cell))) 34 | return melody 35 | 36 | def _get_tone_row(self, row, column): 37 | if column: 38 | return self.matrix[:, column] 39 | return self.matrix[row] 40 | 41 | def save_to_midi(self, tone_rows=1, filename='example.mid'): 42 | m = MIDIFile(filename=filename) 43 | for index in range(0, tone_rows): 44 | row = self.matrix[index] 45 | m.create(row) 46 | 47 | def get_pitch(self, cell): 48 | pitch_map = { 49 | '1': 'C', 50 | '2': 'C# / Db', 51 | '3': 'D', 52 | '4': 'D# / Eb', 53 | '5': 'E', 54 | '6': 'F', 55 | '7': 'F# / Gb', 56 | '8': 'G', 57 | '9': 'G# / Ab', 58 | '10': 'A', 59 | '11': 'A# / Bb', 60 | '12': 'B' 61 | } 62 | 63 | return pitch_map.get(str(cell)) 64 | 65 | def _load_top_row(self, top_row): 66 | row = random.sample(range(1, 13), 12) 67 | # load top row of matrix rows 68 | for x in range(0, 12): 69 | self.matrix[0][x] = top_row[x] if top_row else row[x] 70 | 71 | def _load_first_column(self): 72 | # load first column 73 | for x in range(0, 11): 74 | self._load_col_cell(x) 75 | 76 | def _load_col_cell(self, x): 77 | diff = (self.matrix[0][x + 1] - self.matrix[0][x]) 78 | opposite = diff * -1 79 | result = opposite + self.matrix[x][0] 80 | if result in range(1, 13): 81 | self.matrix[x + 1][0] = result 82 | else: 83 | self.matrix[x + 1][0] = self._transform_cell(result) 84 | 85 | def _compute_matrix(self): 86 | for x in range(1, 12): 87 | for y in range(0, 11): 88 | calc = (self.matrix[x][y] - self.matrix[x - 1][y]) \ 89 | + self.matrix[x - 1][y + 1] 90 | if calc not in range(1, 13): 91 | calc = self._transform_cell(calc) 92 | self.matrix[x][y + 1] = calc 93 | 94 | def _transform_cell(self, cell): 95 | if cell in range(1, 13): 96 | return cell 97 | if cell < 0 or cell == 0: 98 | return self._transform_cell(cell + 12) 99 | else: 100 | return self._transform_cell(cell - 12) 101 | -------------------------------------------------------------------------------- /src/twelve_tone/midi.py: -------------------------------------------------------------------------------- 1 | from miditime.miditime import MIDITime 2 | 3 | 4 | class MIDIFile(object): 5 | 6 | def __init__(self, BPM=120, filename='example.mid'): 7 | self.pattern = MIDITime(BPM, filename) 8 | self.step_counter = 0 9 | self.filename = filename 10 | 11 | def create(self, notes): 12 | midinotes = [] 13 | offset = 60 14 | attack = 200 15 | beats = 1 16 | for note in notes: 17 | pitch = (note - 1) + offset 18 | midinote = [self.step_counter, pitch, attack, beats] 19 | midinotes.append(midinote) 20 | self.step_counter = self.step_counter + 1 21 | 22 | # Add a track with those notes 23 | self.pattern.add_track(midinotes) 24 | 25 | # Output the .mid file 26 | self.pattern.save_midi() 27 | -------------------------------------------------------------------------------- /tests/test_composer.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from twelve_tone.composer import Composer 4 | 5 | 6 | class TestMatrix(unittest.TestCase): 7 | 8 | def test_get_tone_row(self): 9 | m = Composer() 10 | m.compose() 11 | row = m._get_tone_row(1, None) 12 | self.assertEquals(list(row), list(m.matrix[1])) 13 | col = m._get_tone_row(0, 1) 14 | self.assertEquals(list(col), list(m.matrix[:, 1])) 15 | 16 | def test_top_row(self): 17 | m = Composer().compose() 18 | # check top row is unique 19 | duplicate_val = False 20 | if len(m[0]) > len(set(m[0])): 21 | duplicate_val = True 22 | self.assertFalse(duplicate_val) 23 | self.assertEquals(len(m[0]), 12) 24 | 25 | def test_transform_cell(self): 26 | negative = -3 27 | transformed_num = Composer()._transform_cell(negative) 28 | self.assertEqual(transformed_num, 9) 29 | 30 | def test_translate_pitch(self): 31 | cell = 3 32 | pitch = Composer().get_pitch(cell) 33 | self.assertEqual(pitch, 'D') 34 | 35 | def test_master(self): 36 | row = [3, 1, 9, 5, 4, 6, 8, 7, 12, 10, 11, 2] 37 | m = Composer().compose(top_row=row) 38 | self.assertEqual(m[0][0], 3) 39 | self.assertEqual(m[11][0], 4) 40 | self.assertEqual(m[0][11], 2) 41 | self.assertEqual(m[11][11], 3) 42 | # check for 3s all the way diagonal 43 | for x in range(0, 12): 44 | self.assertEqual(m[x][x], 3) 45 | -------------------------------------------------------------------------------- /tests/test_midi.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import unittest 4 | 5 | from twelve_tone.midi import MIDIFile 6 | 7 | 8 | class TestMIDIFile(unittest.TestCase): 9 | 10 | def test_init(self): 11 | m = MIDIFile() 12 | self.assertEquals(m.step_counter, 0) 13 | m = MIDIFile(filename="test.mid") 14 | self.assertEquals(m.filename, 'test.mid') 15 | 16 | def test_create(self): 17 | notes = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] 18 | path = 'tmp' 19 | os.makedirs(path) 20 | os.chdir(path) 21 | m = MIDIFile(filename='test.mid') 22 | m.create(notes) 23 | self.assertTrue(os.path.exists(os.path.join(os.getcwd(), 'test.mid'))) 24 | os.chdir(os.pardir) 25 | shutil.rmtree('tmp', ignore_errors=True) 26 | -------------------------------------------------------------------------------- /tests/test_twelve_tone.py: -------------------------------------------------------------------------------- 1 | 2 | from click.testing import CliRunner 3 | 4 | from twelve_tone.cli import main 5 | 6 | 7 | def test_main(): 8 | runner = CliRunner() 9 | result = runner.invoke(main, []) 10 | 11 | assert result.exit_code == 0 12 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | ; a generative tox configuration, see: https://testrun.org/tox/latest/config.html#generative-envlist 2 | 3 | [tox] 4 | envlist = 5 | clean, 6 | check, 7 | {py27,py33,py34,py35,pypy}, 8 | report, 9 | #docs 10 | 11 | [testenv] 12 | basepython = 13 | pypy: {env:TOXPYTHON:pypy} 14 | {py27,docs,spell}: {env:TOXPYTHON:python2.7} 15 | py33: {env:TOXPYTHON:python3.3} 16 | py34: {env:TOXPYTHON:python3.4} 17 | py35: {env:TOXPYTHON:python3.5} 18 | {clean,check,report,codecov}: python3.5 19 | bootstrap: python 20 | setenv = 21 | PYTHONPATH={toxinidir}/tests 22 | PYTHONUNBUFFERED=yes 23 | passenv = 24 | * 25 | usedevelop = false 26 | deps = 27 | pytest 28 | pytest-travis-fold 29 | pytest-cov 30 | -rrequirements.txt 31 | commands = 32 | {posargs:py.test --cov --cov-report=term-missing -vv tests} 33 | 34 | [testenv:bootstrap] 35 | deps = 36 | jinja2 37 | matrix 38 | skip_install = true 39 | commands = 40 | python ci/bootstrap.py 41 | passenv = 42 | * 43 | 44 | [testenv:spell] 45 | setenv = 46 | SPELLCHECK=1 47 | commands = 48 | sphinx-build -b spelling docs dist/docs 49 | skip_install = true 50 | deps = 51 | -r{toxinidir}/docs/requirements.txt 52 | sphinxcontrib-spelling 53 | pyenchant 54 | 55 | [testenv:docs] 56 | deps = 57 | -r{toxinidir}/docs/requirements.txt 58 | commands = 59 | sphinx-build {posargs:-E} -b html docs dist/docs 60 | sphinx-build -b linkcheck docs dist/docs 61 | 62 | [testenv:check] 63 | deps = 64 | docutils 65 | check-manifest 66 | flake8 67 | readme-renderer 68 | pygments 69 | isort 70 | skip_install = true 71 | commands = 72 | python setup.py check --strict --metadata --restructuredtext 73 | check-manifest {toxinidir} 74 | flake8 src tests setup.py 75 | isort --verbose --check-only --diff --recursive src tests setup.py 76 | 77 | [testenv:coveralls] 78 | deps = 79 | coveralls 80 | skip_install = true 81 | commands = 82 | coverage report 83 | coveralls [] 84 | 85 | [testenv:codecov] 86 | deps = 87 | codecov 88 | skip_install = true 89 | commands = 90 | coverage report 91 | coverage xml --ignore-errors 92 | codecov [] 93 | 94 | 95 | [testenv:report] 96 | deps = coverage 97 | skip_install = true 98 | commands = 99 | coverage report 100 | coverage html 101 | 102 | [testenv:clean] 103 | commands = coverage erase 104 | skip_install = true 105 | deps = coverage 106 | 107 | --------------------------------------------------------------------------------