├── .agignore ├── .editorconfig ├── .github └── workflows │ └── tests.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── CHANGES.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── docs ├── Makefile ├── _static │ └── .gitkeep ├── _templates │ └── .gitkeep ├── conf.py ├── index.rst ├── make.bat ├── pyguitarpro │ ├── api.rst │ ├── format.rst │ ├── install.rst │ └── quickstart.rst ├── requirements.in └── requirements.txt ├── examples ├── dfh.py ├── filter_5_string_bass_tabs.py ├── filter_trio_tabs.py └── transpose.py ├── guitarpro ├── __init__.py ├── gp3.py ├── gp4.py ├── gp5.py ├── io.py ├── iobase.py ├── models.py └── utils.py ├── playground ├── 1 beat.tmp ├── 1 whole bar and 2 beats.tmp ├── 2 beats with lyrics.tmp ├── 2 beats.tmp ├── 2 whole bars.tmp ├── 4 beats with score info.tmp └── 4 beats within bar.tmp ├── pyproject.toml ├── tests ├── 001_Funky_Guy.gp5 ├── 2 whole bars.tmp ├── Chords.gp3 ├── Chords.gp4 ├── Chords.gp5 ├── Demo v5.gp5 ├── Directions.gp5 ├── Duration.gp3 ├── Effects.gp3 ├── Effects.gp4 ├── Effects.gp5 ├── Harmonics.gp3 ├── Harmonics.gp4 ├── Harmonics.gp5 ├── Key.gp4 ├── Key.gp5 ├── Measure Header.gp3 ├── Measure Header.gp4 ├── Measure Header.gp5 ├── No Wah.gp5 ├── RSE.gp5 ├── Repeat.gp4 ├── Repeat.gp5 ├── Slides.gp4 ├── Slides.gp5 ├── Strokes.gp4 ├── Strokes.gp5 ├── Unknown Chord Extension.gp5 ├── Unknown-m.gp5 ├── Unknown.gp5 ├── Vibrato.gp4 ├── Voices.gp5 ├── Wah-m.gp5 ├── Wah.gp5 ├── chord_without_notes.gp5 ├── test_conversion.py └── test_models.py └── tox.ini /.agignore: -------------------------------------------------------------------------------- 1 | tags 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.{yaml,yml}] 11 | indent_style = space 12 | indent_size = 2 13 | 14 | [*.rst] 15 | indent_style = space 16 | indent_size = 3 17 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | push: 4 | branches: [master] 5 | pull_request: 6 | branches: [master] 7 | jobs: 8 | tests: 9 | name: ${{ matrix.name }} 10 | runs-on: ubuntu-latest 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | include: 15 | - {name: '3.12-dev', python: '3.12-dev', tox: py312} 16 | - {name: '3.11', python: '3.11', tox: py311} 17 | - {name: '3.10', python: '3.10', tox: py310} 18 | - {name: '3.9', python: '3.9', tox: py39} 19 | - {name: '3.8', python: '3.8', tox: py38} 20 | - {name: '3.7', python: '3.7', tox: py37} 21 | - {name: PyPy3, python: 'pypy-3.9', tox: pypy39} 22 | - {name: Style, python: '3.11', tox: flake8} 23 | steps: 24 | - uses: actions/checkout@v3 25 | - uses: actions/setup-python@v4 26 | with: 27 | python-version: ${{ matrix.python }} 28 | - name: Update pip 29 | run: | 30 | pip install -U wheel 31 | pip install -U setuptools 32 | python -m pip install -U pip 33 | - run: pip install tox 34 | - run: tox -v -e ${{ matrix.tox }} 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | .DS_Store 3 | 4 | # C extensions 5 | *.so 6 | 7 | # Packages 8 | *.egg 9 | *.egg-info 10 | dist 11 | build 12 | eggs 13 | .eggs 14 | parts 15 | bin 16 | var 17 | sdist 18 | develop-eggs 19 | .installed.cfg 20 | lib 21 | lib64 22 | 23 | # Installer logs 24 | pip-log.txt 25 | 26 | # Unit test / coverage reports 27 | .cache/ 28 | .pytest_cache/ 29 | .coverage 30 | .tox 31 | nosetests.xml 32 | 33 | # Translations 34 | *.mo 35 | 36 | # Built documentation 37 | docs/_build 38 | 39 | # Virtual environment 40 | env 41 | 42 | # Testing output 43 | tests/output 44 | 45 | # Sublime Text files 46 | *.sublime-* 47 | 48 | # ctags 49 | tags 50 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/asottile/pyupgrade 3 | rev: v3.3.1 4 | hooks: 5 | - id: pyupgrade 6 | args: [--py37-plus] 7 | 8 | - repo: https://github.com/PyCQA/flake8 9 | rev: 6.0.0 10 | hooks: 11 | - id: flake8 12 | 13 | - repo: https://github.com/pre-commit/pre-commit-hooks 14 | rev: v4.4.0 15 | hooks: 16 | - id: trailing-whitespace 17 | - id: end-of-file-fixer 18 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | build: 3 | os: ubuntu-22.04 4 | tools: 5 | python: "3.11" 6 | python: 7 | install: 8 | - requirements: docs/requirements.txt 9 | - path: . 10 | sphinx: 11 | configuration: docs/conf.py 12 | formats: 13 | - htmlzip 14 | - epub 15 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | Version 0.9.3 5 | ------------- 6 | 7 | *2023-02-22* 8 | 9 | **Changes:** 10 | 11 | - Fixed ``GraceEffect.durationTime`` `#34 `_. 12 | - Fixed hashing `#31 `_. 13 | - Improved the type checking support for models. 14 | - Fixed ``Song.newMeasure``. 15 | 16 | 17 | Version 0.9.2 18 | ------------- 19 | 20 | *2022-06-26* 21 | 22 | **Changes:** 23 | 24 | - Enabled passing ``bytes`` to the ``parse`` and ``write`` functions. 25 | 26 | 27 | Version 0.9.1 28 | ------------- 29 | 30 | *2022-06-26* 31 | 32 | **Changes:** 33 | 34 | - Enabled passing `PathLike `_ objects to the ``parse`` and 35 | ``write`` functions. 36 | - Fixed reading and writing measure headers in Guitar Pro 5. 37 | 38 | 39 | Version 0.9 40 | ----------- 41 | 42 | *2022-01-23* 43 | 44 | **Backward-incompatible changes:** 45 | 46 | - Aligned the ``GuitarString`` representation with scientific pitch notation. 47 | - Dropped support for Python 3.6. 48 | 49 | **Changes:** 50 | 51 | - Switched to the hash function generated by attrs. 52 | 53 | 54 | Version 0.8 55 | ----------- 56 | 57 | *2020-11-21* 58 | 59 | **Backward-incompatible changes:** 60 | 61 | - Dropped support for Python 2.7. 62 | - Dropped support for Python 3.4. 63 | - Dropped support for Python 3.5. 64 | - Removed the ``Fingering.unknown`` member. 65 | - Removed the misleading ``MeasureHeader.tempo`` and ``Measure.tempo`` attributes. 66 | - Removed the unused ``Beat.index`` attribute. 67 | - Removed the unused ``Color.a`` attribute. 68 | - Removed the unused ``MeasureHeader.hasMarker`` property. 69 | - Removed the unused ``BeatStroke.getIncrementTime`` method. 70 | - Removed the unused ``Tempo`` class. 71 | - Changed the ``Beat.text`` type to ``str`` and removed the ``BeatText`` class. 72 | - Implemented lenient handling of unknown ``Fingering``, ``NoteType``, ``ChordType``, ``ChordExtension`` values 73 | `#18 `_. 74 | - Functions ``guitarpro.parse`` and ``guitarpro.write`` no longer close the passed file object. 75 | 76 | **Changes:** 77 | 78 | - Added type hints. 79 | - Annotated read and write errors with track, measure, voice, and beat numbers. 80 | - Moved the ``end`` property from ``Measure`` to ``MeasureHeader``. 81 | - Truncated the ``MeasureHeader.repeatAlternative`` to 8 bits when writing GP5 file. 82 | - Fixed the vibrato and harmonics when converting a GP3 file to the newer versions. 83 | 84 | 85 | Version 0.7 86 | ----------- 87 | 88 | *2020-09-25* 89 | 90 | **Backward-incompatible changes:** 91 | 92 | - Removed the deprecated ``Beat.realStart`` property. 93 | - Made ``Tuplet.convertTime()`` and ``Duration.time`` return ``int`` for whole times and ``Fraction`` for rational 94 | times. 95 | 96 | **Changes:** 97 | 98 | - Fixed creating durations from dotted tuplets `#16 `_. 99 | 100 | 101 | Version 0.6 102 | ----------- 103 | 104 | *2020-01-19* 105 | 106 | **Backward-incompatible changes:** 107 | 108 | - Removed the deprecated ``Duration.isDoubleDotted`` property. 109 | 110 | **Deprecations:** 111 | 112 | - Deprecated the usage of ``Beat.realStart`` in favor of ``Beat.startInMeasure``. The former will be removed in the next 113 | release. 114 | 115 | **Changes:** 116 | 117 | - Updated `attrs `_ to at least 19.2. 118 | 119 | 120 | Version 0.5 121 | ----------- 122 | 123 | *2018-10-21* 124 | 125 | **Backward-incompatible changes:** 126 | 127 | - Changed the implementation of ``Duration.fromTime`` and removed its ``minimum`` and ``diff`` arguments. Now it raises 128 | a ``ValueError`` if given time cannot be displayed in Guitar Pro. 129 | 130 | **Deprecations:** 131 | 132 | - Deprecated the usage of ``Duration.isDoubleDotted`` because it's not supported in Guitar Pro 3-5. Setting values 133 | to this attribute will issue a warning. The attribute will be completely removed in the next release. 134 | 135 | **Changes:** 136 | 137 | - Fixed a bug that causes chord information to be lost `#10 `_. 138 | - Allowed 13-tuplets to be written. 139 | - Fixed hashing. 140 | - Removed wordy reprs on ``Lyrics``, ``Song``, ``MeasureHeader``, ``TrackRSE``, ``Track``, ``Measure``, ``Voice``, 141 | ``Beat``, ``NoteEffect`` instances. To see an object in somewhat human-readable form use the following snippet: 142 | 143 | .. code-block:: python 144 | 145 | import attr 146 | attr.astuple(track, recurse=False) 147 | 148 | Version 0.4 149 | ----------- 150 | 151 | *2018-04-14* 152 | 153 | **Backward-incompatible changes:** 154 | 155 | - Changed default instantiation behaviour of ``Song``, ``Track``, and ``Measure`` objects `#4 156 | `_. When ``Track`` is created without ``measures`` argument, it 157 | automatically creates a ``MeasureHeader`` with a ``Measure`` that has two ``Voices`` in it. To create a track without 158 | measures, do: 159 | 160 | .. code-block:: python 161 | 162 | track = guitarpro.Track(base_song, measures=[]) 163 | 164 | - Changed how measure headers are compared. Comparing measures won't consider measure headers. Measure headers are 165 | stored in ``Song`` instances, so they will be compared there. 166 | 167 | - Implemented gradual wah-wah changes. There's no ``WahState`` enum, ``WahEffect`` now holds the exact value of wah-wah 168 | pedal position. 169 | 170 | **Changes:** 171 | 172 | - Updated `attrs `_ to at least 17.1. 173 | - Fixed note order in beats before writing. 174 | - Fixed chord reading when there's no fingering. 175 | 176 | 177 | Version 0.3.1 178 | ------------- 179 | 180 | *2017-02-13* 181 | 182 | **Changes:** 183 | 184 | - Made models hashable again. 185 | 186 | 187 | Version 0.3 188 | ----------- 189 | 190 | *2017-02-10* 191 | 192 | **Changes:** 193 | 194 | - Removed ``Note.deadNote`` attribute. 195 | - Fixed track order changes. 196 | - Removed attribute ``Marker.measureHeader``. 197 | - Provided better default values for some models. 198 | - Implemented clipboard files handling. 199 | - Replaced ``GPObject`` with `attrs `_ class decorator. 200 | - Reimplemented version handling. Keyword ``version`` of functions ``parse`` and ``write`` expects a version tuple. 201 | - Moved class ``GPFileBase`` to module ``guitarpro.iobase``, and renamed module ``guitarpro.base`` to 202 | ``guitarpro.models``. 203 | - Exported all models alongside with functions ``parse`` and ``write`` from ``guitarpro`` module. 204 | Now they can be accessed as ``guitarpro.Song``, for example. 205 | - Swapped beat stroke directions. Downstroke is represented by ``BeatStrokeDirection.down`` and upstroke is represented 206 | by ``BeatStrokeDirection.up``. 207 | - Resolved issue `#1 `_. Now it's easier to create a tab from scratch. 208 | 209 | Minor changes: 210 | 211 | - Replaced nosetest with pytest. 212 | 213 | 214 | Version 0.2.2 215 | ------------- 216 | 217 | *2014-04-01* 218 | 219 | **Changes:** 220 | 221 | - Fixed ``NoteType`` enumeration. 222 | - Included examples into sdist. 223 | - Create ``tests.OUTPUT`` directory before running tests. 224 | - Type coercion before writing data (fixes py3k compatibility). 225 | 226 | 227 | Version 0.2.1 228 | ------------- 229 | 230 | *2014-03-30* 231 | 232 | **Changes:** 233 | 234 | - Converted Markdown docs to reST docs. 235 | - Added ``MANIFEST.in``. 236 | 237 | 238 | Version 0.2 239 | ----------- 240 | 241 | *2014-03-30* 242 | 243 | **Changes:** 244 | 245 | - Added Python 3 compatibility. 246 | - Added documentation. 247 | - Added support for RSE. 248 | - Added automated tests using ``nose``. 249 | - Fixed harmonics conversion. 250 | - Converted some classes to ``Enum`` subclasses. 251 | - Added support for chord diagrams. 252 | - Added generic arguments to ``GPObject.__init__``. 253 | - Cleaned up the code. 254 | 255 | 256 | Version 0.1 257 | ----------- 258 | 259 | *2014-03-11* 260 | 261 | First public release. 262 | 263 | .. vim: tw=120 cc=121 264 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. [http://fsf.org/] 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | include CHANGES.rst 4 | graft tests 5 | graft examples 6 | graft docs 7 | prune docs/_build 8 | prune docs/_themes 9 | prune tests/output 10 | global-exclude *.py[co] 11 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | PyGuitarPro 2 | =========== 3 | 4 | .. image:: https://img.shields.io/pypi/v/pyguitarpro.svg?style=flat 5 | :alt: PyPI Package latest release 6 | :target: https://pypi.org/project/PyGuitarPro/ 7 | 8 | 9 | Introduction 10 | ------------ 11 | 12 | PyGuitarPro is a package to read, write and manipulate GP3, GP4 and GP5 files. Initially PyGuitarPro is a Python port 13 | of `AlphaTab `_ which originally was a Haxe port of 14 | `TuxGuitar `_. 15 | 16 | This package helps you achieve several goals you might find yourself yearning to do in a day-to-day tabber life: 17 | 18 | - transpose a track without messing the fingering. 19 | 20 | - add first string to the track without messing the fingering. 21 | 22 | - map percussion notes to different values. 23 | 24 | Reading ``.gp*`` files is as easy as: 25 | 26 | .. code-block:: python 27 | 28 | import guitarpro 29 | curl = guitarpro.parse('Mastodon - Curl of the Burl.gp5') 30 | 31 | Writing ``.gp*`` files isn't that hard as well: 32 | 33 | .. code-block:: python 34 | 35 | guitarpro.write(curl, 'Mastodon - Curl of the Burl 2.gp5') 36 | 37 | All objects representing GP entities are hashable and comparable. This gives the great opportunity to apply *diff* 38 | algorithm to tabs, or even *diff3* algorithm to merge tablatures. 39 | 40 | To anyone wanting to create their the best guitar tablature editor in Python this package will be the good thing to 41 | start with. 42 | 43 | 44 | Examples 45 | -------- 46 | 47 | Several usage examples are included in the ``/examples`` folder. Please feel free to add your own examples, or improve 48 | on some of the existing ones, and then submit them via pull request. 49 | 50 | To run one of the examples in your local environment, simply: 51 | 52 | .. code-block:: sh 53 | 54 | cd pyguitarpro 55 | python examples/transpose.py --help 56 | 57 | 58 | Installation 59 | ------------ 60 | 61 | Install PyGuitarPro from PyPI: 62 | 63 | .. code-block:: sh 64 | 65 | pip install PyGuitarPro 66 | 67 | To install development version of PyGuitarPro: 68 | 69 | .. code-block:: sh 70 | 71 | git clone https://github.com/Perlence/PyGuitarPro.git 72 | cd pyguitarpro 73 | pip install -e . 74 | 75 | 76 | Documentation 77 | ------------- 78 | 79 | Package documentation is located at `Read the Docs `_. 80 | 81 | 82 | Licensing 83 | --------- 84 | 85 | Please see the file called ``LICENSE``. 86 | 87 | .. vim: tw=120 cc=121 88 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/_static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/docs/_static/.gitkeep -------------------------------------------------------------------------------- /docs/_templates/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/docs/_templates/.gitkeep -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # PyGuitarPro documentation build configuration file, created by 2 | # sphinx-quickstart on Tue Mar 18 15:03:30 2014. 3 | # 4 | # This file is execfile()d with the current directory set to its containing dir. 5 | # 6 | # Note that not all possible configuration values are present in this 7 | # autogenerated file. 8 | # 9 | # All configuration values have a default; values that are commented out 10 | # serve to show the default. 11 | 12 | # -- Path setup -------------------------------------------------------------- 13 | 14 | # If extensions (or modules to document with autodoc) are in another directory, 15 | # add these directories to sys.path here. If the directory is relative to the 16 | # documentation root, use os.path.abspath to make it absolute, like shown here. 17 | 18 | import os 19 | import sys 20 | sys.path.insert(0, os.path.abspath('..')) 21 | 22 | 23 | # -- General configuration ----------------------------------------------------- 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | # needs_sphinx = '1.0' 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be extensions 29 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 30 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 31 | 'sphinx.ext.coverage', 'sphinx.ext.viewcode'] 32 | 33 | # Add any paths that contain templates here, relative to this directory. 34 | templates_path = ['_templates'] 35 | 36 | # The suffix of source filenames. 37 | source_suffix = '.rst' 38 | 39 | # The encoding of source files. 40 | # source_encoding = 'utf-8-sig' 41 | 42 | # The master toctree document. 43 | master_doc = 'index' 44 | 45 | # General information about the project. 46 | project = 'PyGuitarPro' 47 | copyright = '2014, Sviatoslav Abakumov' 48 | 49 | # The version info for the project you're documenting, acts as replacement for 50 | # |version| and |release|, also used in various other places throughout the 51 | # built documents. 52 | # 53 | # The short X.Y version. 54 | version = '0.9.3' 55 | # The full version, including alpha/beta/rc tags. 56 | release = '0.9.3' 57 | 58 | # The language for content autogenerated by Sphinx. Refer to documentation 59 | # for a list of supported languages. 60 | # language = None 61 | 62 | # There are two options for replacing |today|: either, you set today to some 63 | # non-false value, then it is used: 64 | # today = '' 65 | # Else, today_fmt is used as the format for a strftime call. 66 | # today_fmt = '%B %d, %Y' 67 | 68 | # List of patterns, relative to source directory, that match files and 69 | # directories to ignore when looking for source files. 70 | exclude_patterns = ['_build'] 71 | 72 | # The reST default role (used for this markup: `text`) to use for all documents. 73 | # default_role = None 74 | 75 | # If true, '()' will be appended to :func: etc. cross-reference text. 76 | # add_function_parentheses = True 77 | 78 | # If true, the current module name will be prepended to all description 79 | # unit titles (such as .. function::). 80 | # add_module_names = True 81 | 82 | # If true, sectionauthor and moduleauthor directives will be shown in the 83 | # output. They are ignored by default. 84 | # show_authors = False 85 | 86 | # The name of the Pygments (syntax highlighting) style to use. 87 | pygments_style = 'sphinx' 88 | 89 | # A list of ignored prefixes for module index sorting. 90 | # modindex_common_prefix = [] 91 | 92 | 93 | # -- Options for HTML output --------------------------------------------------- 94 | 95 | on_rtd = os.environ.get('READTHEDOCS', None) == 'True' 96 | 97 | if not on_rtd: 98 | import sphinx_rtd_theme 99 | html_theme = 'sphinx_rtd_theme' 100 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 101 | 102 | # The name for this set of Sphinx documents. If None, it defaults to 103 | # " v documentation". 104 | # html_title = None 105 | 106 | # A shorter title for the navigation bar. Default is the same as html_title. 107 | # html_short_title = None 108 | 109 | # The name of an image file (relative to this directory) to place at the top 110 | # of the sidebar. 111 | # html_logo = None 112 | 113 | # The name of an image file (within the static path) to use as favicon of the 114 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 115 | # pixels large. 116 | # html_favicon = None 117 | 118 | # Add any paths that contain custom static files (such as style sheets) here, 119 | # relative to this directory. They are copied after the builtin static files, 120 | # so a file named "default.css" will overwrite the builtin "default.css". 121 | html_static_path = ['_static'] 122 | 123 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 124 | # using the given strftime format. 125 | # html_last_updated_fmt = '%b %d, %Y' 126 | 127 | # If true, SmartyPants will be used to convert quotes and dashes to 128 | # typographically correct entities. 129 | # html_use_smartypants = True 130 | 131 | # Custom sidebar templates, maps document names to template names. 132 | # html_sidebars = {} 133 | 134 | # Additional templates that should be rendered to pages, maps page names to 135 | # template names. 136 | # html_additional_pages = {} 137 | 138 | # If false, no module index is generated. 139 | # html_domain_indices = True 140 | 141 | # If false, no index is generated. 142 | # html_use_index = True 143 | 144 | # If true, the index is split into individual pages for each letter. 145 | # html_split_index = False 146 | 147 | # If true, links to the reST sources are added to the pages. 148 | # html_show_sourcelink = True 149 | 150 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 151 | # html_show_sphinx = True 152 | 153 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 154 | # html_show_copyright = True 155 | 156 | # If true, an OpenSearch description file will be output, and all pages will 157 | # contain a tag referring to it. The value of this option must be the 158 | # base URL from which the finished HTML is served. 159 | # html_use_opensearch = '' 160 | 161 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 162 | # html_file_suffix = None 163 | 164 | # Output file base name for HTML help builder. 165 | htmlhelp_basename = 'PyGuitarProdoc' 166 | 167 | 168 | # -- Options for LaTeX output -------------------------------------------------- 169 | 170 | latex_elements = { 171 | # The paper size ('letterpaper' or 'a4paper'). 172 | # 'papersize': 'letterpaper', 173 | 174 | # The font size ('10pt', '11pt' or '12pt'). 175 | # 'pointsize': '10pt', 176 | 177 | # Additional stuff for the LaTeX preamble. 178 | # 'preamble': '', 179 | } 180 | 181 | # Grouping the document tree into LaTeX files. List of tuples 182 | # (source start file, target name, title, author, documentclass [howto/manual]). 183 | latex_documents = [ 184 | ('index', 'PyGuitarPro.tex', 'PyGuitarPro Documentation', 185 | 'Sviatoslav Abakumov', 'manual'), 186 | ] 187 | 188 | # The name of an image file (relative to this directory) to place at the top of 189 | # the title page. 190 | # latex_logo = None 191 | 192 | # For "manual" documents, if this is true, then toplevel headings are parts, 193 | # not chapters. 194 | # latex_use_parts = False 195 | 196 | # If true, show page references after internal links. 197 | # latex_show_pagerefs = False 198 | 199 | # If true, show URL addresses after external links. 200 | # latex_show_urls = False 201 | 202 | # Documents to append as an appendix to all manuals. 203 | # latex_appendices = [] 204 | 205 | # If false, no module index is generated. 206 | # latex_domain_indices = True 207 | 208 | 209 | # -- Options for manual page output -------------------------------------------- 210 | 211 | # One entry per manual page. List of tuples 212 | # (source start file, name, description, authors, manual section). 213 | man_pages = [ 214 | ('index', 'pyguitarpro', 'PyGuitarPro Documentation', 215 | ['Sviatoslav Abakumov'], 1) 216 | ] 217 | 218 | # If true, show URL addresses after external links. 219 | # man_show_urls = False 220 | 221 | 222 | # -- Options for Texinfo output ------------------------------------------------ 223 | 224 | # Grouping the document tree into Texinfo files. List of tuples 225 | # (source start file, target name, title, author, 226 | # dir menu entry, description, category) 227 | texinfo_documents = [ 228 | ('index', 'PyGuitarPro', 'PyGuitarPro Documentation', 229 | 'Sviatoslav Abakumov', 'PyGuitarPro', 'One line description of project.', 230 | 'Miscellaneous'), 231 | ] 232 | 233 | # Documents to append as an appendix to all manuals. 234 | # texinfo_appendices = [] 235 | 236 | # If false, no module index is generated. 237 | # texinfo_domain_indices = True 238 | 239 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 240 | # texinfo_show_urls = 'footnote' 241 | 242 | 243 | # Example configuration for intersphinx: refer to the Python standard library. 244 | intersphinx_mapping = {'http://docs.python.org/': None} 245 | 246 | autodoc_member_order = 'bysource' 247 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. _contents: 2 | 3 | PyGuitarPro 4 | =========== 5 | 6 | PyGuitarPro is a package to read, write and manipulate GP3, GP4 and GP5 files. Initially PyGuitarPro is a Python port 7 | of `AlphaTab `_ which is a Haxe port of `TuxGuitar `_. 8 | 9 | To anyone wanting to create their own the best guitar tablature editor in Python this package will be the good thing to 10 | start with. 11 | 12 | .. toctree:: 13 | :maxdepth: 2 14 | 15 | pyguitarpro/install 16 | pyguitarpro/quickstart 17 | pyguitarpro/api 18 | pyguitarpro/format 19 | 20 | 21 | Indices and tables 22 | ================== 23 | 24 | - :ref:`genindex` 25 | - :ref:`modindex` 26 | - :ref:`search` 27 | 28 | .. vim: tw=120 cc=121 29 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/pyguitarpro/api.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ============= 3 | 4 | General functions 5 | ----------------- 6 | 7 | .. autofunction:: guitarpro.parse 8 | 9 | .. autofunction:: guitarpro.write 10 | 11 | 12 | Models 13 | ------ 14 | 15 | .. automodule:: guitarpro.models 16 | :members: 17 | 18 | .. vim: tw=120 cc=121 19 | -------------------------------------------------------------------------------- /docs/pyguitarpro/format.rst: -------------------------------------------------------------------------------- 1 | Guitar Pro File Format 2 | ====================== 3 | 4 | 5 | Basic Guitar Pro 3—5 types 6 | -------------------------- 7 | 8 | .. _byte: 9 | 10 | Byte 11 | ^^^^ 12 | 13 | Values of type :ref:`byte` are stored in 1 byte. 14 | 15 | 16 | .. _signed-byte: 17 | 18 | Signed byte 19 | ^^^^^^^^^^^ 20 | 21 | Values of type :ref:`signed-byte` are stored in 1 byte. 22 | 23 | 24 | .. _bool: 25 | 26 | Bool 27 | ^^^^ 28 | 29 | Values of type :ref:`bool` are stored in 1 byte. 30 | 31 | 32 | .. _short: 33 | 34 | Short 35 | ^^^^^ 36 | 37 | Values of type :ref:`short` are stored in 2 little-endian bytes. 38 | 39 | 40 | .. _int: 41 | 42 | Int 43 | ^^^ 44 | 45 | Values of type :ref:`int` are stored in 4 little-endian bytes. 46 | 47 | 48 | .. _float: 49 | 50 | Float 51 | ^^^^^ 52 | 53 | Values of type :ref:`float` are stored in 4 little-endian bytes. 54 | 55 | 56 | .. _double: 57 | 58 | Double 59 | ^^^^^^ 60 | 61 | Values of type :ref:`double` are stored in 8 little-endian bytes. 62 | 63 | 64 | .. _byte-size-string: 65 | 66 | ByteSizeString 67 | ^^^^^^^^^^^^^^ 68 | 69 | Values of type :ref:`byte-size-string` are represented by length of the string (1 :ref:`byte`) followed by characters 70 | encoded in an 8-bit charset. 71 | 72 | 73 | .. _int-size-string: 74 | 75 | IntSizeString 76 | ^^^^^^^^^^^^^ 77 | 78 | Values of type :ref:`int-size-string` are represented by length of the string (1 :ref:`int`) followed by characters 79 | encoded in an 8-bit charset. 80 | 81 | 82 | .. _int-byte-size-string: 83 | 84 | IntByteSizeString 85 | ^^^^^^^^^^^^^^^^^ 86 | 87 | Values of type :ref:`int-byte-size-string` are represented by an :ref:`int` holding length of the string increased by 1, 88 | a :ref:`byte` holding length of the string and finally followed by characters encoded in an 8-bit charset. 89 | 90 | 91 | Guitar Pro 3 format 92 | ------------------- 93 | 94 | 95 | .. automodule:: guitarpro.gp3 96 | :members: 97 | 98 | 99 | Guitar Pro 4 format 100 | ------------------- 101 | 102 | .. automodule:: guitarpro.gp4 103 | :members: 104 | 105 | 106 | Guitar Pro 5 format 107 | ------------------- 108 | 109 | .. automodule:: guitarpro.gp5 110 | :members: 111 | 112 | .. vim: tw=120 cc=121 113 | -------------------------------------------------------------------------------- /docs/pyguitarpro/install.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Install the package with: 5 | 6 | .. code-block:: sh 7 | 8 | pip install pyguitarpro 9 | 10 | For the latest development version: 11 | 12 | .. code-block:: sh 13 | 14 | git clone https://github.com/Perlence/PyGuitarPro 15 | cd pyguitarpro 16 | pip install -e . 17 | 18 | .. vim: tw=120 cc=121 19 | -------------------------------------------------------------------------------- /docs/pyguitarpro/quickstart.rst: -------------------------------------------------------------------------------- 1 | Quickstart 2 | ========== 3 | 4 | After the package has been installed, it's ready for hacking. 5 | 6 | Reading ``.gp*`` files is as easy as: 7 | 8 | .. code-block:: python 9 | 10 | import guitarpro 11 | demo = guitarpro.parse('Demo v5.gp5') 12 | 13 | Writing ``.gp*`` files isn't that hard as well: 14 | 15 | .. code-block:: python 16 | 17 | guitarpro.write(demo, 'Demo v5 2.gp5') 18 | 19 | All objects representing GP entities are hashable and comparable. This gives the great opportunity to apply *diff* 20 | algorithm to tabs, or even *diff3* algorithm to merge tablatures. 21 | 22 | The package is also designed to convert tablatures between formats. To do this, simply change extension of the output 23 | file according to your desired format: 24 | 25 | .. code-block:: python 26 | 27 | guitarpro.write(demo, 'Demo v5.gp4') 28 | 29 | Functions :func:`guitarpro.parse` and :func:`guitarpro.write` support not only filenames, but also file-like object: 30 | 31 | .. code-block:: python 32 | 33 | from urllib.request import urlopen 34 | with urlopen('https://github.com/Perlence/PyGuitarPro/raw/master/tests/Demo%20v5.gp5') as stream: 35 | demo = guitarpro.parse(stream) 36 | 37 | .. note:: 38 | 39 | PyGuitarPro supports only GP3, GP4 and GP5 files. Support for GPX (Guitar Pro 6) files is out of scope of the 40 | project. 41 | 42 | .. vim: tw=120 cc=121 43 | -------------------------------------------------------------------------------- /docs/requirements.in: -------------------------------------------------------------------------------- 1 | sphinx 2 | sphinx_rtd_theme 3 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.11 3 | # by the following command: 4 | # 5 | # pip-compile docs/requirements.in 6 | # 7 | alabaster==0.7.13 8 | # via sphinx 9 | babel==2.11.0 10 | # via sphinx 11 | certifi==2022.12.7 12 | # via requests 13 | charset-normalizer==3.0.1 14 | # via requests 15 | docutils==0.18.1 16 | # via 17 | # sphinx 18 | # sphinx-rtd-theme 19 | idna==3.4 20 | # via requests 21 | imagesize==1.4.1 22 | # via sphinx 23 | jinja2==3.1.2 24 | # via sphinx 25 | markupsafe==2.1.2 26 | # via jinja2 27 | packaging==23.0 28 | # via sphinx 29 | pygments==2.14.0 30 | # via sphinx 31 | pytz==2022.7.1 32 | # via babel 33 | requests==2.28.2 34 | # via sphinx 35 | snowballstemmer==2.2.0 36 | # via sphinx 37 | sphinx==6.1.3 38 | # via 39 | # -r docs/requirements.in 40 | # sphinx-rtd-theme 41 | sphinx-rtd-theme==1.2.0 42 | # via -r docs/requirements.in 43 | sphinxcontrib-applehelp==1.0.4 44 | # via sphinx 45 | sphinxcontrib-devhelp==1.0.2 46 | # via sphinx 47 | sphinxcontrib-htmlhelp==2.0.1 48 | # via sphinx 49 | sphinxcontrib-jquery==2.0.0 50 | # via sphinx-rtd-theme 51 | sphinxcontrib-jsmath==1.0.1 52 | # via sphinx 53 | sphinxcontrib-qthelp==1.0.3 54 | # via sphinx 55 | sphinxcontrib-serializinghtml==1.1.5 56 | # via sphinx 57 | urllib3==1.26.14 58 | # via requests 59 | 60 | # The following packages are considered to be unsafe in a requirements file: 61 | # setuptools 62 | -------------------------------------------------------------------------------- /examples/dfh.py: -------------------------------------------------------------------------------- 1 | from os import path 2 | 3 | import guitarpro 4 | 5 | 6 | MAPPING = { 7 | 22: 42, 8 | 23: 42, 9 | 24: 46, 10 | 25: 46, 11 | 26: 46, 12 | 27: 52, 13 | 28: 49, 14 | 29: 52, 15 | 30: 56, 16 | 31: 57, 17 | 32: 57, 18 | 33: 38, 19 | 54: 49, 20 | 58: 57, 21 | } 22 | 23 | 24 | def main(source, dest=None, tracks=None): 25 | song = guitarpro.parse(source) 26 | 27 | if tracks is None: 28 | # Process all percussion tracks. 29 | tracks = (track for track in song.tracks if track.isPercussionTrack) 30 | else: 31 | # Get tracks by track numbers. 32 | tracks = (song.tracks[n] for n in tracks) 33 | 34 | for track in tracks: 35 | # Map values to Genaral MIDI. 36 | for measure in track.measures: 37 | for voice in measure.voices: 38 | for beat in voice.beats: 39 | for note in beat.notes: 40 | note.value = MAPPING.get(note.value, note.value) 41 | 42 | # Extend note durations to remove rests in-between. 43 | voiceparts = zip(*(measure.voices for measure in track.measures)) 44 | for measures in voiceparts: 45 | for measure in measures: 46 | last = None 47 | newbeats = [] 48 | for beat in measure.beats: 49 | if beat.notes: 50 | last = beat 51 | elif last is not None: 52 | try: 53 | newduration = guitarpro.Duration.fromTime(last.duration.time + beat.duration.time) 54 | except ValueError: 55 | last = beat 56 | else: 57 | last.duration = newduration 58 | continue 59 | newbeats.append(beat) 60 | measure.beats = newbeats 61 | 62 | if dest is None: 63 | dest = '%s-generalized%s' % path.splitext(source) 64 | guitarpro.write(song, dest) 65 | 66 | 67 | if __name__ == '__main__': 68 | import argparse 69 | 70 | description = """ 71 | Replace Drumkit from Hell specific values to General MIDI ones and 72 | remove rests by extending beats. 73 | """ 74 | parser = argparse.ArgumentParser(description=description) 75 | parser.add_argument('source', 76 | metavar='SOURCE', 77 | help='path to the source tab') 78 | parser.add_argument('dest', 79 | metavar='DEST', nargs='?', 80 | help='path to the processed tab') 81 | parser.add_argument('-t', '--track', 82 | metavar='NUMBER', type=int, dest='tracks', 83 | action='append', 84 | help='zero-based number of the track to transpose') 85 | args = parser.parse_args() 86 | kwargs = dict(args._get_kwargs()) 87 | main(**kwargs) 88 | -------------------------------------------------------------------------------- /examples/filter_5_string_bass_tabs.py: -------------------------------------------------------------------------------- 1 | import os 2 | import fnmatch 3 | 4 | import guitarpro 5 | 6 | 7 | def main(source): 8 | print("Filtering...\n") 9 | supportedExtensions = '*.gp[345]' 10 | 11 | for dirpath, dirs, files in os.walk(source): 12 | for file in fnmatch.filter(files, supportedExtensions): 13 | guitarProPath = os.path.join(dirpath, file) 14 | try: 15 | tab = guitarpro.parse(guitarProPath) 16 | except guitarpro.GPException as exception: 17 | print("###This is not a supported GuitarPro file:", guitarProPath, ":", exception) 18 | else: 19 | for track in tab.tracks: 20 | if not track.isPercussionTrack: 21 | if len(track.strings) == 5: 22 | print(guitarProPath) 23 | print("\nDone!") 24 | 25 | 26 | if __name__ == '__main__': 27 | import argparse 28 | description = "List Guitar Pro files containing 5 string bass track." 29 | parser = argparse.ArgumentParser(description=description) 30 | parser.add_argument('source', 31 | metavar='SOURCE', 32 | help='path to the source tabs folder') 33 | args = parser.parse_args() 34 | kwargs = dict(args._get_kwargs()) 35 | main(**kwargs) 36 | -------------------------------------------------------------------------------- /examples/filter_trio_tabs.py: -------------------------------------------------------------------------------- 1 | import os 2 | import fnmatch 3 | 4 | import guitarpro 5 | 6 | 7 | def main(source): 8 | print("Filtering...\n") 9 | supportedExtensions = '*.gp[345]' 10 | 11 | for dirpath, dirs, files in os.walk(source): 12 | for file in fnmatch.filter(files, supportedExtensions): 13 | guitarProPath = os.path.join(dirpath, file) 14 | try: 15 | tab = guitarpro.parse(guitarProPath) 16 | except guitarpro.GPException as exc: 17 | print("###This is not a supported Guitar Pro file:", guitarProPath, ":", exc) 18 | else: 19 | if isABassGuitarDrumsFile(tab): 20 | print(guitarProPath) 21 | print("\nDone!") 22 | 23 | 24 | def isABassGuitarDrumsFile(tab): 25 | drumsOK = bassOK = guitarOK = False 26 | 27 | if len(tab.tracks) == 3: 28 | for track in tab.tracks: 29 | if not track.isPercussionTrack: 30 | if len(track.strings) <= 5: 31 | bassOK = True 32 | else: 33 | guitarOK = True 34 | else: 35 | drumsOK = True 36 | 37 | return drumsOK and bassOK and guitarOK 38 | 39 | 40 | if __name__ == '__main__': 41 | import argparse 42 | description = ("List Guitar Pro files containing three tracks: " 43 | "bass, guitar and drums.") 44 | parser = argparse.ArgumentParser(description=description) 45 | parser.add_argument('source', 46 | metavar='SOURCE', 47 | help='path to the source tabs folder') 48 | args = parser.parse_args() 49 | kwargs = dict(args._get_kwargs()) 50 | main(**kwargs) 51 | -------------------------------------------------------------------------------- /examples/transpose.py: -------------------------------------------------------------------------------- 1 | from os import path 2 | 3 | import guitarpro 4 | 5 | 6 | def unfold_tracknumber(tracknumber, tracks): 7 | """Substitute '*' with all track numbers except for percussion 8 | tracks. 9 | """ 10 | if tracknumber == '*': 11 | for number, track in enumerate(tracks, start=1): 12 | if not track.isPercussionTrack: 13 | yield number 14 | else: 15 | yield tracknumber 16 | 17 | 18 | def process(track, measure, voice, beat, note, semitone, stringmap): 19 | if (1 << (note.string - 1) & stringmap and 20 | not (note.type == guitarpro.NoteType.dead or 21 | note.type == guitarpro.NoteType.tie)): 22 | note.value += semitone 23 | capped = max(0, min(track.fretCount, note.value)) 24 | if note.value != capped: 25 | print("Warning on track %d '%s', measure %d" % (track.number, track.name, measure.number)) 26 | note.type = guitarpro.NoteType.dead 27 | note.value = capped 28 | return note 29 | 30 | 31 | def transpose(track, semitone, stringmap): 32 | for measure in track.measures: 33 | for voice in measure.voices: 34 | for beat in voice.beats: 35 | for note in beat.notes: 36 | note = process(track, measure, voice, beat, note, semitone, stringmap) 37 | 38 | 39 | def main(source, dest, tracks, semitones, stringmaps): 40 | if tracks is None: 41 | tracks = ['*'] 42 | if stringmaps is None: 43 | stringmaps = [0x7f] * len(tracks) 44 | song = guitarpro.parse(source) 45 | for number, semitone, stringmap in zip(tracks, semitones, stringmaps): 46 | for number in unfold_tracknumber(number, song.tracks): 47 | track = song.tracks[number - 1] 48 | transpose(track, semitone, stringmap) 49 | if dest is None: 50 | dest = '%s-transposed%s' % path.splitext(source) 51 | guitarpro.write(song, dest) 52 | 53 | 54 | if __name__ == '__main__': 55 | import argparse 56 | 57 | def bitarray(string): 58 | return int(string, base=2) 59 | 60 | def tracknumber(string): 61 | if string == '*': 62 | return string 63 | else: 64 | return int(string) 65 | 66 | description = """ 67 | Transpose tracks of GP tab by N semitones. 68 | Multiple '--track' and '--by' arguments can be specified. 69 | """ 70 | parser = argparse.ArgumentParser(description=description) 71 | parser.add_argument('source', 72 | metavar='SOURCE', 73 | help='path to the source tab') 74 | parser.add_argument('dest', 75 | metavar='DEST', nargs='?', 76 | help='path to the processed tab') 77 | parser.add_argument('-t', '--track', 78 | metavar='NUMBER', type=tracknumber, dest='tracks', 79 | action='append', 80 | help='number of the track to transpose') 81 | parser.add_argument('-b', '--by', 82 | metavar='N', type=int, required=True, dest='semitones', 83 | action='append', 84 | help='transpose by N steps') 85 | parser.add_argument('-s', '--stringmap', 86 | metavar='BITARRAY', type=bitarray, dest='stringmaps', 87 | action='append', 88 | help='bit array where ones represent strings that ' 89 | 'should be transposed, e.g. `100000` will ' 90 | 'transpose only 6th string') 91 | args = parser.parse_args() 92 | kwargs = dict(args._get_kwargs()) 93 | main(**kwargs) 94 | -------------------------------------------------------------------------------- /guitarpro/__init__.py: -------------------------------------------------------------------------------- 1 | from .io import parse, write # noqa 2 | from .models import * # noqa 3 | 4 | __version__ = '0.9.3' 5 | -------------------------------------------------------------------------------- /guitarpro/gp4.py: -------------------------------------------------------------------------------- 1 | import attr 2 | 3 | from . import models as gp 4 | from . import gp3 5 | from .utils import clamp 6 | 7 | 8 | class GP4File(gp3.GP3File): 9 | """A reader for GuitarPro 4 files.""" 10 | 11 | # Reading 12 | # ======= 13 | 14 | def readSong(self): 15 | """Read the song. 16 | 17 | A song consists of score information, triplet feel, lyrics, 18 | tempo, song key, MIDI channels, measure and track count, measure 19 | headers, tracks, measures. 20 | 21 | - Version: :ref:`byte-size-string` of size 30. 22 | 23 | - Score information. 24 | See :meth:`readInfo`. 25 | 26 | - Triplet feel: :ref:`bool`. 27 | If value is true, then triplet feel is set to eigth. 28 | 29 | - Lyrics. See :meth:`readLyrics`. 30 | 31 | - Tempo: :ref:`int`. 32 | 33 | - Key: :ref:`int`. Key signature of the song. 34 | 35 | - Octave: :ref:`signed-byte`. Reserved for future uses. 36 | 37 | - MIDI channels. See :meth:`readMidiChannels`. 38 | 39 | - Number of measures: :ref:`int`. 40 | 41 | - Number of tracks: :ref:`int`. 42 | 43 | - Measure headers. See :meth:`readMeasureHeaders`. 44 | 45 | - Tracks. See :meth:`readTracks`. 46 | 47 | - Measures. See :meth:`readMeasures`. 48 | """ 49 | song = gp.Song(tracks=[], measureHeaders=[]) 50 | song.version = self.readVersion() 51 | song.versionTuple = self.versionTuple 52 | song.clipboard = self.readClipboard() 53 | 54 | self.readInfo(song) 55 | self._tripletFeel = gp.TripletFeel.eighth if self.readBool() else gp.TripletFeel.none 56 | song.lyrics = self.readLyrics() 57 | song.tempo = self.readInt() 58 | song.key = gp.KeySignature((self.readInt(), 0)) 59 | self.readSignedByte() # octave 60 | channels = self.readMidiChannels() 61 | measureCount = self.readInt() 62 | trackCount = self.readInt() 63 | with self.annotateErrors('reading'): 64 | self.readMeasureHeaders(song, measureCount) 65 | self.readTracks(song, trackCount, channels) 66 | self.readMeasures(song) 67 | return song 68 | 69 | def readClipboard(self): 70 | if not self.isClipboard(): 71 | return 72 | clipboard = gp.Clipboard() 73 | clipboard.startMeasure = self.readInt() 74 | clipboard.stopMeasure = self.readInt() 75 | clipboard.startTrack = self.readInt() 76 | clipboard.stopTrack = self.readInt() 77 | return clipboard 78 | 79 | def isClipboard(self): 80 | return self.version.startswith('CLIPBOARD') 81 | 82 | def readLyrics(self): 83 | """Read lyrics. 84 | 85 | First, read an :ref:`int` that points to the track lyrics are 86 | bound to. Then it is followed by 5 lyric lines. Each one 87 | constists of number of starting measure encoded in :ref:`int` 88 | and :ref:`int-size-string` holding text of the lyric line. 89 | """ 90 | lyrics = gp.Lyrics() 91 | lyrics.trackChoice = self.readInt() 92 | for line in lyrics.lines: 93 | line.startingMeasure = self.readInt() 94 | line.lyrics = self.readIntSizeString() 95 | return lyrics 96 | 97 | def packMeasureHeaderFlags(self, header, previous=None): 98 | flags = super().packMeasureHeaderFlags(header, previous) 99 | if previous is None or header.keySignature != previous.keySignature: 100 | flags |= 0x40 101 | if header.hasDoubleBar: 102 | flags |= 0x80 103 | return flags 104 | 105 | def writeMeasureHeaderValues(self, header, flags): 106 | super().writeMeasureHeaderValues(header, flags) 107 | if flags & 0x40: 108 | self.writeSignedByte(header.keySignature.value[0]) 109 | self.writeSignedByte(header.keySignature.value[1]) 110 | 111 | def readNewChord(self, chord): 112 | """Read new-style (GP4) chord diagram. 113 | 114 | New-style chord diagram is read as follows: 115 | 116 | - Sharp: :ref:`bool`. If true, display all semitones as sharps, 117 | otherwise display as flats. 118 | 119 | - Blank space, 3 :ref:`Bytes `. 120 | 121 | - Root: :ref:`byte`. Values are: 122 | 123 | * -1 for customized chords 124 | * 0: C 125 | * 1: C# 126 | * ... 127 | 128 | - Type: :ref:`byte`. Determines the chord type as followed. See 129 | :class:`guitarpro.models.ChordType` for mapping. 130 | 131 | - Chord extension: :ref:`byte`. See 132 | :class:`guitarpro.models.ChordExtension` for mapping. 133 | 134 | - Bass note: :ref:`int`. Lowest note of chord as in *C/Am*. 135 | 136 | - Tonality: :ref:`int`. See 137 | :class:`guitarpro.models.ChordAlteration` for mapping. 138 | 139 | - Add: :ref:`bool`. Determines if an "add" (added note) is 140 | present in the chord. 141 | 142 | - Name: :ref:`byte-size-string`. Max length is 22. 143 | 144 | - Fifth tonality: :ref:`byte`. Maps to 145 | :class:`guitarpro.models.ChordExtension`. 146 | 147 | - Ninth tonality: :ref:`byte`. Maps to 148 | :class:`guitarpro.models.ChordExtension`. 149 | 150 | - Eleventh tonality: :ref:`byte`. Maps to 151 | :class:`guitarpro.models.ChordExtension`. 152 | 153 | - List of frets: 6 :ref:`Ints `. Fret values are saved as 154 | in default format. 155 | 156 | - Count of barres: :ref:`byte`. Maximum count is 5. 157 | 158 | - Barre frets: 5 :ref:`Bytes `. 159 | 160 | - Barre start strings: 5 :ref:`Bytes `. 161 | 162 | - Barre end string: 5 :ref:`Bytes `. 163 | 164 | - Omissions: 7 :ref:`Bools `. If the value is true then 165 | note is played in chord. 166 | 167 | - Blank space, 1 :ref:`byte`. 168 | 169 | - Fingering: 7 :ref:`SignedBytes `. For value 170 | mapping, see :class:`guitarpro.models.Fingering`. 171 | """ 172 | chord.sharp = self.readBool() 173 | intonation = 'sharp' if chord.sharp else 'flat' 174 | self.skip(3) 175 | chord.root = gp.PitchClass(self.readByte(), intonation=intonation) 176 | chord.type = gp.ChordType(self.readByte()) 177 | chord.extension = gp.ChordExtension(self.readByte()) 178 | chord.bass = gp.PitchClass(self.readInt(), intonation=intonation) 179 | chord.tonality = gp.ChordAlteration(self.readInt()) 180 | chord.add = self.readBool() 181 | chord.name = self.readByteSizeString(22) 182 | chord.fifth = gp.ChordAlteration(self.readByte()) 183 | chord.ninth = gp.ChordAlteration(self.readByte()) 184 | chord.eleventh = gp.ChordAlteration(self.readByte()) 185 | chord.firstFret = self.readInt() 186 | for i in range(7): 187 | fret = self.readInt() 188 | if i < len(chord.strings): 189 | chord.strings[i] = fret 190 | chord.barres = [] 191 | barresCount = self.readByte() 192 | barreFrets = self.readByte(5) 193 | barreStarts = self.readByte(5) 194 | barreEnds = self.readByte(5) 195 | for fret, start, end, _ in zip(barreFrets, barreStarts, barreEnds, range(barresCount)): 196 | barre = gp.Barre(fret, start, end) 197 | chord.barres.append(barre) 198 | chord.omissions = self.readBool(7) 199 | self.skip(1) 200 | chord.fingerings = list(map(gp.Fingering, self.readSignedByte(7))) 201 | chord.show = self.readBool() 202 | 203 | def readBeatEffects(self, noteEffect): 204 | """Read beat effects. 205 | 206 | Beat effects are read using two byte flags. 207 | 208 | The first byte of flags is: 209 | 210 | - *0x01*: *blank* 211 | - *0x02*: wide vibrato 212 | - *0x04*: *blank* 213 | - *0x08*: *blank* 214 | - *0x10*: fade in 215 | - *0x20*: slap effect 216 | - *0x40*: beat stroke 217 | - *0x80*: *blank* 218 | 219 | The second byte of flags is: 220 | 221 | - *0x01*: rasgueado 222 | - *0x02*: pick stroke 223 | - *0x04*: tremolo bar 224 | - *0x08*: *blank* 225 | - *0x10*: *blank* 226 | - *0x20*: *blank* 227 | - *0x40*: *blank* 228 | - *0x80*: *blank* 229 | 230 | Flags are followed by: 231 | 232 | - Slap effect: :ref:`signed-byte`. For value mapping see 233 | :class:`guitarpro.models.SlapEffect`. 234 | 235 | - Tremolo bar. See :meth:`readTremoloBar`. 236 | 237 | - Beat stroke. See :meth:`readBeatStroke`. 238 | 239 | - Pick stroke: :ref:`signed-byte`. For value mapping see 240 | :class:`guitarpro.models.BeatStrokeDirection`. 241 | """ 242 | beatEffect = gp.BeatEffect() 243 | flags1 = self.readSignedByte() 244 | flags2 = self.readSignedByte() 245 | beatEffect.vibrato = bool(flags1 & 0x02) or beatEffect.vibrato 246 | beatEffect.fadeIn = bool(flags1 & 0x10) 247 | if flags1 & 0x20: 248 | value = self.readSignedByte() 249 | beatEffect.slapEffect = gp.SlapEffect(value) 250 | if flags2 & 0x04: 251 | beatEffect.tremoloBar = self.readTremoloBar() 252 | if flags1 & 0x40: 253 | beatEffect.stroke = self.readBeatStroke() 254 | beatEffect.hasRasgueado = bool(flags2 & 0x01) 255 | if flags2 & 0x02: 256 | direction = self.readSignedByte() 257 | beatEffect.pickStroke = gp.BeatStrokeDirection(direction) 258 | return beatEffect 259 | 260 | def readTremoloBar(self): 261 | return self.readBend() 262 | 263 | def readMixTableChange(self, measure): 264 | """Read mix table change. 265 | 266 | Mix table change in Guitar Pro 4 format extends Guitar Pro 3 267 | format. It constists of :meth:`values 268 | `, 269 | :meth:`durations 270 | `, and, new 271 | to GP3, :meth:`flags `. 272 | """ 273 | tableChange = super().readMixTableChange(measure) 274 | self.readMixTableChangeFlags(tableChange) 275 | return tableChange 276 | 277 | def readMixTableChangeFlags(self, tableChange): 278 | """Read mix table change flags. 279 | 280 | The meaning of flags: 281 | 282 | - *0x01*: change volume for all tracks 283 | - *0x02*: change balance for all tracks 284 | - *0x04*: change chorus for all tracks 285 | - *0x08*: change reverb for all tracks 286 | - *0x10*: change phaser for all tracks 287 | - *0x20*: change tremolo for all tracks 288 | """ 289 | flags = self.readSignedByte() 290 | if tableChange.volume is not None: 291 | tableChange.volume.allTracks = bool(flags & 0x01) 292 | if tableChange.balance is not None: 293 | tableChange.balance.allTracks = bool(flags & 0x02) 294 | if tableChange.chorus is not None: 295 | tableChange.chorus.allTracks = bool(flags & 0x04) 296 | if tableChange.reverb is not None: 297 | tableChange.reverb.allTracks = bool(flags & 0x08) 298 | if tableChange.phaser is not None: 299 | tableChange.phaser.allTracks = bool(flags & 0x10) 300 | if tableChange.tremolo is not None: 301 | tableChange.tremolo.allTracks = bool(flags & 0x20) 302 | return flags 303 | 304 | def readNoteEffects(self, note): 305 | """Read note effects. 306 | 307 | The effects presence for the current note is set by the 2 bytes 308 | of flags. 309 | 310 | First set of flags: 311 | 312 | - *0x01*: bend 313 | - *0x02*: hammer-on/pull-off 314 | - *0x04*: *blank* 315 | - *0x08*: let-ring 316 | - *0x10*: grace note 317 | - *0x20*: *blank* 318 | - *0x40*: *blank* 319 | - *0x80*: *blank* 320 | 321 | Second set of flags: 322 | 323 | - *0x01*: staccato 324 | - *0x02*: palm mute 325 | - *0x04*: tremolo picking 326 | - *0x08*: slide 327 | - *0x10*: harmonic 328 | - *0x20*: trill 329 | - *0x40*: vibrato 330 | - *0x80*: *blank* 331 | 332 | Flags are followed by: 333 | 334 | - Bend. See :meth:`readBend`. 335 | 336 | - Grace note. See :meth:`readGrace`. 337 | 338 | - Tremolo picking. See :meth:`readTremoloPicking`. 339 | 340 | - Slide. See :meth:`readSlides`. 341 | 342 | - Harmonic. See :meth:`readHarmonic`. 343 | 344 | - Trill. See :meth:`readTrill`. 345 | """ 346 | noteEffect = note.effect or gp.NoteEffect() 347 | flags1 = self.readSignedByte() 348 | flags2 = self.readSignedByte() 349 | noteEffect.hammer = bool(flags1 & 0x02) 350 | noteEffect.letRing = bool(flags1 & 0x08) 351 | noteEffect.staccato = bool(flags2 & 0x01) 352 | noteEffect.palmMute = bool(flags2 & 0x02) 353 | noteEffect.vibrato = bool(flags2 & 0x40) or noteEffect.vibrato 354 | if flags1 & 0x01: 355 | noteEffect.bend = self.readBend() 356 | if flags1 & 0x10: 357 | noteEffect.grace = self.readGrace() 358 | if flags2 & 0x04: 359 | noteEffect.tremoloPicking = self.readTremoloPicking() 360 | if flags2 & 0x08: 361 | noteEffect.slides = self.readSlides() 362 | if flags2 & 0x10: 363 | noteEffect.harmonic = self.readHarmonic(note) 364 | if flags2 & 0x20: 365 | noteEffect.trill = self.readTrill() 366 | return noteEffect 367 | 368 | def readTremoloPicking(self): 369 | """Read tremolo picking. 370 | 371 | Tremolo constists of picking speed encoded in 372 | :ref:`signed-byte`. For value mapping refer to 373 | :meth:`fromTremoloValue`. 374 | """ 375 | value = self.readSignedByte() 376 | tp = gp.TremoloPickingEffect() 377 | tp.duration.value = self.fromTremoloValue(value) 378 | return tp 379 | 380 | def fromTremoloValue(self, value): 381 | """Convert tremolo picking speed to actual duration. 382 | 383 | Values are: 384 | 385 | - *1*: eighth 386 | - *2*: sixteenth 387 | - *3*: thirtySecond 388 | """ 389 | if value == 1: 390 | return gp.Duration.eighth 391 | elif value == 2: 392 | return gp.Duration.sixteenth 393 | elif value == 3: 394 | return gp.Duration.thirtySecond 395 | 396 | def readSlides(self): 397 | """Read slides. 398 | 399 | Slide is encoded in :ref:`signed-byte`. See 400 | :class:`guitarpro.models.SlideType` for value mapping. 401 | """ 402 | return [gp.SlideType(self.readSignedByte())] 403 | 404 | def readHarmonic(self, note): 405 | """Read harmonic. 406 | 407 | Harmonic is encoded in :ref:`signed-byte`. Values correspond to: 408 | 409 | - *1*: natural harmonic 410 | - *3*: tapped harmonic 411 | - *4*: pinch harmonic 412 | - *5*: semi-harmonic 413 | - *15*: artificial harmonic on (*n + 5*)th fret 414 | - *17*: artificial harmonic on (*n + 7*)th fret 415 | - *22*: artificial harmonic on (*n + 12*)th fret 416 | """ 417 | harmonicType = self.readSignedByte() 418 | if harmonicType == 1: 419 | harmonic = gp.NaturalHarmonic() 420 | elif harmonicType == 3: 421 | harmonic = gp.TappedHarmonic() 422 | elif harmonicType == 4: 423 | harmonic = gp.PinchHarmonic() 424 | elif harmonicType == 5: 425 | harmonic = gp.SemiHarmonic() 426 | elif harmonicType == 15: 427 | pitch = gp.PitchClass((note.realValue + 7) % 12) 428 | octave = gp.Octave.ottava 429 | harmonic = gp.ArtificialHarmonic(pitch, octave) 430 | elif harmonicType == 17: 431 | pitch = gp.PitchClass(note.realValue) 432 | octave = gp.Octave.quindicesima 433 | harmonic = gp.ArtificialHarmonic(pitch, octave) 434 | elif harmonicType == 22: 435 | pitch = gp.PitchClass(note.realValue) 436 | octave = gp.Octave.ottava 437 | harmonic = gp.ArtificialHarmonic(pitch, octave) 438 | return harmonic 439 | 440 | def readTrill(self): 441 | """Read trill. 442 | 443 | - Fret: :ref:`signed-byte`. 444 | 445 | - Period: :ref:`signed-byte`. See :meth:`fromTrillPeriod`. 446 | """ 447 | trill = gp.TrillEffect() 448 | trill.fret = self.readSignedByte() 449 | trill.duration.value = self.fromTrillPeriod(self.readSignedByte()) 450 | return trill 451 | 452 | def fromTrillPeriod(self, period): 453 | """Convert trill period to actual duration. 454 | 455 | Values are: 456 | 457 | - *1*: sixteenth 458 | - *2*: thirty-second 459 | - *3*: sixty-fourth 460 | """ 461 | if period == 1: 462 | return gp.Duration.sixteenth 463 | elif period == 2: 464 | return gp.Duration.thirtySecond 465 | elif period == 3: 466 | return gp.Duration.sixtyFourth 467 | 468 | # Writing 469 | # ======= 470 | 471 | def writeSong(self, song): 472 | self.writeVersion() 473 | self.writeClipboard(song.clipboard) 474 | 475 | self.writeInfo(song) 476 | self._tripletFeel = song.tracks[0].measures[0].tripletFeel.value 477 | self.writeBool(self._tripletFeel) 478 | self.writeLyrics(song.lyrics) 479 | 480 | self.writeInt(song.tempo) 481 | self.writeInt(song.key.value[0]) 482 | self.writeSignedByte(0) # octave 483 | 484 | self.writeMidiChannels(song.tracks) 485 | 486 | measureCount = len(song.tracks[0].measures) 487 | trackCount = len(song.tracks) 488 | self.writeInt(measureCount) 489 | self.writeInt(trackCount) 490 | 491 | with self.annotateErrors('writing'): 492 | self.writeMeasureHeaders(song.tracks[0].measures) 493 | self.writeTracks(song.tracks) 494 | self.writeMeasures(song.tracks) 495 | 496 | def writeClipboard(self, clipboard): 497 | if clipboard is None: 498 | return 499 | self.writeInt(clipboard.startMeasure) 500 | self.writeInt(clipboard.stopMeasure) 501 | self.writeInt(clipboard.startTrack) 502 | self.writeInt(clipboard.stopTrack) 503 | 504 | def writeLyrics(self, lyrics): 505 | self.writeInt(lyrics.trackChoice) 506 | for line in lyrics.lines: 507 | self.writeInt(line.startingMeasure) 508 | self.writeIntSizeString(line.lyrics) 509 | 510 | def writeBeat(self, beat): 511 | flags = 0x00 512 | if beat.duration.isDotted: 513 | flags |= 0x01 514 | if beat.effect.isChord: 515 | flags |= 0x02 516 | if beat.text is not None: 517 | flags |= 0x04 518 | if not beat.effect.isDefault: 519 | flags |= 0x08 520 | if beat.effect.mixTableChange is not None: 521 | if not beat.effect.mixTableChange.isJustWah or self.versionTuple[0] > 4: 522 | flags |= 0x10 523 | if beat.duration.tuplet != gp.Tuplet(): 524 | flags |= 0x20 525 | if beat.status != gp.BeatStatus.normal: 526 | flags |= 0x40 527 | self.writeSignedByte(flags) 528 | if flags & 0x40: 529 | self.writeByte(beat.status.value) 530 | self.writeDuration(beat.duration, flags) 531 | if flags & 0x02: 532 | self.writeChord(beat.effect.chord) 533 | if flags & 0x04: 534 | self.writeIntByteSizeString(beat.text) 535 | if flags & 0x08: 536 | self.writeBeatEffects(beat) 537 | if flags & 0x10: 538 | self.writeMixTableChange(beat.effect.mixTableChange) 539 | self.writeNotes(beat) 540 | 541 | def writeChord(self, chord): 542 | self.writeSignedByte(1) # signify GP4 chord format 543 | self.writeBool(chord.sharp) 544 | self.placeholder(3) 545 | self.writeByte(chord.root.value if chord.root else 0) 546 | self.writeByte(self.getEnumValue(chord.type) if chord.type else 0) 547 | self.writeByte(self.getEnumValue(chord.extension) if chord.extension else 0) 548 | self.writeInt(chord.bass.value if chord.bass else 0) 549 | self.writeInt(chord.tonality.value if chord.tonality else 0) 550 | self.writeBool(chord.add) 551 | self.writeByteSizeString(chord.name, 22) 552 | self.writeByte(chord.fifth.value if chord.fifth else 0) 553 | self.writeByte(chord.ninth.value if chord.ninth else 0) 554 | self.writeByte(chord.eleventh.value if chord.eleventh else 0) 555 | 556 | self.writeInt(chord.firstFret) 557 | for fret in clamp(chord.strings, 7, fillvalue=-1): 558 | self.writeInt(fret) 559 | 560 | self.writeByte(len(chord.barres)) 561 | if chord.barres: 562 | barreFrets, barreStarts, barreEnds = zip(*map(attr.astuple, chord.barres)) 563 | else: 564 | barreFrets, barreStarts, barreEnds = [], [], [] 565 | for fret in clamp(barreFrets, 5, fillvalue=0): 566 | self.writeByte(fret) 567 | for start in clamp(barreStarts, 5, fillvalue=0): 568 | self.writeByte(start) 569 | for end in clamp(barreEnds, 5, fillvalue=0): 570 | self.writeByte(end) 571 | 572 | for omission in clamp(chord.omissions, 7, fillvalue=True): 573 | self.writeBool(omission) 574 | 575 | self.placeholder(1) 576 | placeholder = gp.Fingering(-2) 577 | for fingering in clamp(chord.fingerings, 7, fillvalue=placeholder): 578 | self.writeSignedByte(fingering.value) 579 | self.writeBool(chord.show) 580 | 581 | def writeBeatEffects(self, beat): 582 | flags1 = 0x00 583 | if beat.effect.vibrato: 584 | flags1 |= 0x02 585 | if beat.effect.fadeIn: 586 | flags1 |= 0x10 587 | if beat.effect.isSlapEffect: 588 | flags1 |= 0x20 589 | if beat.effect.stroke != gp.BeatStroke(): 590 | flags1 |= 0x40 591 | 592 | self.writeSignedByte(flags1) 593 | 594 | flags2 = 0x00 595 | if beat.effect.hasRasgueado: 596 | flags2 |= 0x01 597 | if beat.effect.hasPickStroke: 598 | flags2 |= 0x02 599 | if beat.effect.isTremoloBar: 600 | flags2 |= 0x04 601 | 602 | self.writeSignedByte(flags2) 603 | 604 | if flags1 & 0x20: 605 | self.writeSignedByte(beat.effect.slapEffect.value) 606 | if flags2 & 0x04: 607 | self.writeTremoloBar(beat.effect.tremoloBar) 608 | if flags1 & 0x40: 609 | self.writeBeatStroke(beat.effect.stroke) 610 | if flags2 & 0x02: 611 | self.writeSignedByte(beat.effect.pickStroke.value) 612 | 613 | def writeTremoloBar(self, tremoloBar): 614 | self.writeBend(tremoloBar) 615 | 616 | def writeMixTableChange(self, tableChange): 617 | super().writeMixTableChange(tableChange) 618 | self.writeMixTableChangeFlags(tableChange) 619 | 620 | def writeMixTableChangeFlags(self, tableChange): 621 | flags = 0x00 622 | if tableChange.volume is not None and tableChange.volume.allTracks: 623 | flags |= 0x01 624 | if tableChange.balance is not None and tableChange.balance.allTracks: 625 | flags |= 0x02 626 | if tableChange.chorus is not None and tableChange.chorus.allTracks: 627 | flags |= 0x04 628 | if tableChange.reverb is not None and tableChange.reverb.allTracks: 629 | flags |= 0x08 630 | if tableChange.phaser is not None and tableChange.phaser.allTracks: 631 | flags |= 0x10 632 | if tableChange.tremolo is not None and tableChange.tremolo.allTracks: 633 | flags |= 0x20 634 | self.writeSignedByte(flags) 635 | 636 | def writeNote(self, note): 637 | flags = self.packNoteFlags(note) 638 | self.writeByte(flags) 639 | if flags & 0x20: 640 | self.writeByte(self.getEnumValue(note.type)) 641 | if flags & 0x01: 642 | self.writeSignedByte(note.duration) 643 | self.writeSignedByte(note.tuplet) 644 | if flags & 0x10: 645 | value = self.packVelocity(note.velocity) 646 | self.writeSignedByte(value) 647 | if flags & 0x20: 648 | fret = note.value if note.type != gp.NoteType.tie else 0 649 | self.writeSignedByte(fret) 650 | if flags & 0x80: 651 | self.writeSignedByte(self.getEnumValue(note.effect.leftHandFinger)) 652 | self.writeSignedByte(self.getEnumValue(note.effect.rightHandFinger)) 653 | if flags & 0x08: 654 | self.writeNoteEffects(note) 655 | 656 | def packNoteFlags(self, note): 657 | flags = super().packNoteFlags(note) 658 | if note.effect.accentuatedNote: 659 | flags |= 0x40 660 | if note.effect.isFingering: 661 | flags |= 0x80 662 | return flags 663 | 664 | def writeNoteEffects(self, note): 665 | noteEffect = note.effect 666 | flags1 = 0x00 667 | if noteEffect.isBend: 668 | flags1 |= 0x01 669 | if noteEffect.hammer: 670 | flags1 |= 0x02 671 | if noteEffect.letRing: 672 | flags1 |= 0x08 673 | if noteEffect.isGrace: 674 | flags1 |= 0x10 675 | self.writeSignedByte(flags1) 676 | flags2 = 0x00 677 | if noteEffect.staccato: 678 | flags2 |= 0x01 679 | if noteEffect.palmMute: 680 | flags2 |= 0x02 681 | if noteEffect.isTremoloPicking: 682 | flags2 |= 0x04 683 | if noteEffect.slides: 684 | flags2 |= 0x08 685 | if noteEffect.isHarmonic: 686 | flags2 |= 0x10 687 | if noteEffect.isTrill: 688 | flags2 |= 0x20 689 | if noteEffect.vibrato: 690 | flags2 |= 0x40 691 | self.writeSignedByte(flags2) 692 | if flags1 & 0x01: 693 | self.writeBend(noteEffect.bend) 694 | if flags1 & 0x10: 695 | self.writeGrace(noteEffect.grace) 696 | if flags2 & 0x04: 697 | self.writeTremoloPicking(noteEffect.tremoloPicking) 698 | if flags2 & 0x08: 699 | self.writeSlides(noteEffect.slides) 700 | if flags2 & 0x10: 701 | self.writeHarmonic(note, noteEffect.harmonic) 702 | if flags2 & 0x20: 703 | self.writeTrill(noteEffect.trill) 704 | 705 | def writeTremoloPicking(self, tremoloPicking): 706 | self.writeSignedByte(self.toTremoloValue(tremoloPicking.duration.value)) 707 | 708 | def writeSlides(self, slides): 709 | self.writeSignedByte(slides[0].value) 710 | 711 | def toTremoloValue(self, value): 712 | if value == gp.Duration.eighth: 713 | return 1 714 | elif value == gp.Duration.sixteenth: 715 | return 2 716 | elif value == gp.Duration.thirtySecond: 717 | return 3 718 | 719 | def writeHarmonic(self, note, harmonic): 720 | if not isinstance(harmonic, gp.ArtificialHarmonic): 721 | byte = harmonic.type 722 | else: 723 | if harmonic.pitch and harmonic.octave: 724 | if harmonic.pitch.value == (note.realValue + 7) % 12 and harmonic.octave == gp.Octave.ottava: 725 | byte = 15 726 | elif harmonic.pitch.value == note.realValue % 12 and harmonic.octave == gp.Octave.quindicesima: 727 | byte = 17 728 | elif harmonic.pitch.value == note.realValue % 12 and harmonic.octave == gp.Octave.ottava: 729 | byte = 22 730 | else: 731 | byte = 22 732 | else: 733 | byte = 22 734 | self.writeSignedByte(byte) 735 | 736 | def writeTrill(self, trill): 737 | self.writeSignedByte(trill.fret) 738 | self.writeSignedByte(self.toTrillPeriod(trill.duration.value)) 739 | 740 | def toTrillPeriod(self, value): 741 | if value == gp.Duration.sixteenth: 742 | return 1 743 | if value == gp.Duration.thirtySecond: 744 | return 2 745 | if value == gp.Duration.sixtyFourth: 746 | return 3 747 | -------------------------------------------------------------------------------- /guitarpro/gp5.py: -------------------------------------------------------------------------------- 1 | import attr 2 | 3 | from . import models as gp 4 | from . import gp4 5 | 6 | 7 | class GP5File(gp4.GP4File): 8 | """A reader for GuitarPro 5 files.""" 9 | 10 | # Reading 11 | # ======= 12 | 13 | def readSong(self): 14 | """Read the song. 15 | 16 | A song consists of score information, triplet feel, lyrics, 17 | tempo, song key, MIDI channels, measure and track count, measure 18 | headers, tracks, measures. 19 | 20 | - Version: :ref:`byte-size-string` of size 30. 21 | 22 | - Score information. 23 | See :meth:`readInfo`. 24 | 25 | - Lyrics. See :meth:`readLyrics`. 26 | 27 | - RSE master effect. See :meth:`readRSEInstrument`. 28 | 29 | - Tempo name: :ref:`int-byte-size-string`. 30 | 31 | - Tempo: :ref:`int`. 32 | 33 | - Hide tempo: :ref:`bool`. Don't display tempo on the sheet if 34 | set. 35 | 36 | - Key: :ref:`int`. Key signature of the song. 37 | 38 | - Octave: :ref:`int`. Octave of the song. 39 | 40 | - MIDI channels. See :meth:`readMidiChannels`. 41 | 42 | - Directions. See :meth:`readDirections`. 43 | 44 | - Master reverb. See :meth:`readMasterReverb`. 45 | 46 | - Number of measures: :ref:`int`. 47 | 48 | - Number of tracks: :ref:`int`. 49 | 50 | - Measure headers. See :meth:`readMeasureHeaders`. 51 | 52 | - Tracks. See :meth:`readTracks`. 53 | 54 | - Measures. See :meth:`readMeasures`. 55 | """ 56 | song = gp.Song(tracks=[], measureHeaders=[]) 57 | song.version = self.readVersion() 58 | song.versionTuple = self.versionTuple 59 | 60 | if self.isClipboard(): 61 | song.clipboard = self.readClipboard() 62 | 63 | self.readInfo(song) 64 | song.lyrics = self.readLyrics() 65 | song.masterEffect = self.readRSEMasterEffect() 66 | song.pageSetup = self.readPageSetup() 67 | song.tempoName = self.readIntByteSizeString() 68 | song.tempo = self.readInt() 69 | song.hideTempo = self.readBool() if self.versionTuple > (5, 0, 0) else False 70 | song.key = gp.KeySignature((self.readSignedByte(), 0)) 71 | self.readInt() # octave 72 | channels = self.readMidiChannels() 73 | directions = self.readDirections() 74 | song.masterEffect.reverb = self.readInt() 75 | measureCount = self.readInt() 76 | trackCount = self.readInt() 77 | with self.annotateErrors('reading'): 78 | self.readMeasureHeaders(song, measureCount, directions) 79 | self.readTracks(song, trackCount, channels) 80 | self.readMeasures(song) 81 | return song 82 | 83 | def readClipboard(self): 84 | clipboard = super().readClipboard() 85 | if clipboard is None: 86 | return 87 | clipboard.startBeat = self.readInt() 88 | clipboard.stopBeat = self.readInt() 89 | clipboard.subBarCopy = bool(self.readInt()) 90 | return clipboard 91 | 92 | def readInfo(self, song): 93 | """Read score information. 94 | 95 | Score information consists of sequence of 96 | :ref:`IntByteSizeStrings `: 97 | 98 | - title 99 | - subtitle 100 | - artist 101 | - album 102 | - words 103 | - music 104 | - copyright 105 | - tabbed by 106 | - instructions 107 | 108 | The sequence if followed by notice. Notice starts with the 109 | number of notice lines stored in :ref:`int`. Each line is 110 | encoded in :ref:`int-byte-size-string`. 111 | """ 112 | song.title = self.readIntByteSizeString() 113 | song.subtitle = self.readIntByteSizeString() 114 | song.artist = self.readIntByteSizeString() 115 | song.album = self.readIntByteSizeString() 116 | song.words = self.readIntByteSizeString() 117 | song.music = self.readIntByteSizeString() 118 | song.copyright = self.readIntByteSizeString() 119 | song.tab = self.readIntByteSizeString() 120 | song.instructions = self.readIntByteSizeString() 121 | notesCount = self.readInt() 122 | song.notice = [] 123 | for _ in range(notesCount): 124 | song.notice.append(self.readIntByteSizeString()) 125 | 126 | def readRSEMasterEffect(self): 127 | """Read RSE master effect. 128 | 129 | Persistence of RSE master effect was introduced in Guitar Pro 130 | 5.1. It is read as: 131 | 132 | - Master volume: :ref:`int`. Values are in range from 0 to 200. 133 | 134 | - 10-band equalizer. See :meth:`readEqualizer`. 135 | """ 136 | masterEffect = gp.RSEMasterEffect() 137 | if self.versionTuple > (5, 0, 0): 138 | masterEffect.volume = self.readInt() 139 | self.readInt() # ??? 140 | masterEffect.equalizer = self.readEqualizer(11) 141 | return masterEffect 142 | 143 | def readEqualizer(self, knobsNumber): 144 | """Read equalizer values. 145 | 146 | Equalizers are used in RSE master effect and Track RSE. They 147 | consist of *n* :ref:`SignedBytes ` for each *n* 148 | bands and one :ref:`signed-byte` for gain (PRE) fader. 149 | 150 | Volume values are stored as opposite to actual value. See 151 | :meth:`unpackVolumeValue`. 152 | """ 153 | knobs = list(map(self.unpackVolumeValue, self.readSignedByte(count=knobsNumber))) 154 | return gp.RSEEqualizer(knobs=knobs[:-1], gain=knobs[-1]) 155 | 156 | def unpackVolumeValue(self, value): 157 | """Unpack equalizer volume value. 158 | 159 | Equalizer volumes are float but stored as 160 | :ref:`SignedBytes `. 161 | """ 162 | return -value / 10 163 | 164 | def readPageSetup(self): 165 | """Read page setup. 166 | 167 | Page setup is read as follows: 168 | 169 | - Page size: 2 :ref:`Ints `. Width and height of the page. 170 | 171 | - Page padding: 4 :ref:`Ints `. Left, right, top, bottom 172 | padding of the page. 173 | 174 | - Score size proportion: :ref:`int`. 175 | 176 | - Header and footer elements: :ref:`short`. See 177 | :class:`guitarpro.models.HeaderFooterElements` for value 178 | mapping. 179 | 180 | - List of placeholders: 181 | 182 | * title 183 | * subtitle 184 | * artist 185 | * album 186 | * words 187 | * music 188 | * wordsAndMusic 189 | * copyright1, e.g. *"Copyright %copyright%"* 190 | * copyright2, e.g. *"All Rights Reserved - International 191 | Copyright Secured"* 192 | * pageNumber 193 | """ 194 | setup = gp.PageSetup() 195 | setup.pageSize = gp.Point(self.readInt(), self.readInt()) 196 | left = self.readInt() 197 | right = self.readInt() 198 | top = self.readInt() 199 | bottom = self.readInt() 200 | setup.pageMargin = gp.Padding(left, top, right, bottom) 201 | setup.scoreSizeProportion = self.readInt() / 100 202 | setup.headerAndFooter = self.readShort() 203 | setup.title = self.readIntByteSizeString() 204 | setup.subtitle = self.readIntByteSizeString() 205 | setup.artist = self.readIntByteSizeString() 206 | setup.album = self.readIntByteSizeString() 207 | setup.words = self.readIntByteSizeString() 208 | setup.music = self.readIntByteSizeString() 209 | setup.wordsAndMusic = self.readIntByteSizeString() 210 | setup.copyright = self.readIntByteSizeString() + '\n' + self.readIntByteSizeString() 211 | setup.pageNumber = self.readIntByteSizeString() 212 | return setup 213 | 214 | def readDirections(self): 215 | """Read directions. 216 | 217 | Directions is a list of 19 :ref:`ShortInts ` each 218 | pointing at the number of measure. 219 | 220 | Directions are read in the following order. 221 | 222 | - Coda 223 | - Double Coda 224 | - Segno 225 | - Segno Segno 226 | - Fine 227 | - Da Capo 228 | - Da Capo al Coda 229 | - Da Capo al Double Coda 230 | - Da Capo al Fine 231 | - Da Segno 232 | - Da Segno al Coda 233 | - Da Segno al Double Coda 234 | - Da Segno al Fine 235 | - Da Segno Segno 236 | - Da Segno Segno al Coda 237 | - Da Segno Segno al Double Coda 238 | - Da Segno Segno al Fine 239 | - Da Coda 240 | - Da Double Coda 241 | """ 242 | signs = { 243 | gp.DirectionSign('Coda'): self.readShort(), 244 | gp.DirectionSign('Double Coda'): self.readShort(), 245 | gp.DirectionSign('Segno'): self.readShort(), 246 | gp.DirectionSign('Segno Segno'): self.readShort(), 247 | gp.DirectionSign('Fine'): self.readShort() 248 | } 249 | fromSigns = { 250 | gp.DirectionSign('Da Capo'): self.readShort(), 251 | gp.DirectionSign('Da Capo al Coda'): self.readShort(), 252 | gp.DirectionSign('Da Capo al Double Coda'): self.readShort(), 253 | gp.DirectionSign('Da Capo al Fine'): self.readShort(), 254 | gp.DirectionSign('Da Segno'): self.readShort(), 255 | gp.DirectionSign('Da Segno al Coda'): self.readShort(), 256 | gp.DirectionSign('Da Segno al Double Coda'): self.readShort(), 257 | gp.DirectionSign('Da Segno al Fine'): self.readShort(), 258 | gp.DirectionSign('Da Segno Segno'): self.readShort(), 259 | gp.DirectionSign('Da Segno Segno al Coda'): self.readShort(), 260 | gp.DirectionSign('Da Segno Segno al Double Coda'): self.readShort(), 261 | gp.DirectionSign('Da Segno Segno al Fine'): self.readShort(), 262 | gp.DirectionSign('Da Coda'): self.readShort(), 263 | gp.DirectionSign('Da Double Coda'): self.readShort() 264 | } 265 | return signs, fromSigns 266 | 267 | def readMeasureHeaders(self, song, measureCount, directions): 268 | super().readMeasureHeaders(song, measureCount) 269 | signs, fromSigns = directions 270 | for sign, number in signs.items(): 271 | if number > -1: 272 | song.measureHeaders[number - 1].direction = sign 273 | for sign, number in fromSigns.items(): 274 | if number > -1: 275 | song.measureHeaders[number - 1].fromDirection = sign 276 | 277 | def readMeasureHeader(self, number, song, previous=None): 278 | """Read measure header. 279 | 280 | Measure header format in Guitar Pro 5 differs from one in Guitar 281 | Pro 3. 282 | 283 | First, there is a blank byte if measure is not first. Then 284 | measure header is read as in GP3's 285 | :meth:`guitarpro.gp3.readMeasureHeader`. Then measure header is 286 | read as follows: 287 | 288 | - Time signature beams: 4 :ref:`Bytes `. Appears If time 289 | signature was set, i.e. flags *0x01* and *0x02* are both set. 290 | 291 | - Blank :ref:`byte` if flag at *0x10* is set. 292 | 293 | - Triplet feel: :ref:`byte`. See 294 | :class:`guitarpro.models.TripletFeel`. 295 | """ 296 | if previous is not None: 297 | # Always 0 298 | self.skip(1) 299 | 300 | flags = self.readByte() 301 | header = gp.MeasureHeader() 302 | header.number = number 303 | header.start = 0 304 | header.tripletFeel = self._tripletFeel 305 | if flags & 0x01: 306 | header.timeSignature.numerator = self.readSignedByte() 307 | else: 308 | header.timeSignature.numerator = previous.timeSignature.numerator 309 | if flags & 0x02: 310 | header.timeSignature.denominator.value = self.readSignedByte() 311 | else: 312 | header.timeSignature.denominator.value = previous.timeSignature.denominator.value 313 | header.isRepeatOpen = bool(flags & 0x04) 314 | if flags & 0x08: 315 | header.repeatClose = self.readSignedByte() 316 | if flags & 0x20: 317 | header.marker = self.readMarker(header) 318 | if flags & 0x40: 319 | root = self.readSignedByte() 320 | type_ = self.readSignedByte() 321 | header.keySignature = gp.KeySignature((root, type_)) 322 | elif header.number > 1: 323 | header.keySignature = previous.keySignature 324 | if flags & 0x10: 325 | header.repeatAlternative = self.readRepeatAlternative(song.measureHeaders) 326 | header.hasDoubleBar = bool(flags & 0x80) 327 | 328 | if header.repeatClose > -1: 329 | header.repeatClose -= 1 330 | if flags & 0x03: 331 | header.timeSignature.beams = self.readByte(4) 332 | else: 333 | header.timeSignature.beams = previous.timeSignature.beams 334 | if flags & 0x10 == 0: 335 | # Always 0 336 | self.skip(1) 337 | header.tripletFeel = gp.TripletFeel(self.readByte()) 338 | return header, flags 339 | 340 | def readRepeatAlternative(self, measureHeaders): 341 | return self.readByte() 342 | 343 | def readTracks(self, song, trackCount, channels): 344 | """Read tracks. 345 | 346 | Tracks in Guitar Pro 5 have almost the same format as in Guitar 347 | Pro 3. If it's Guitar Pro 5.0 then 2 blank bytes are read after 348 | :meth:`guitarpro.gp3.readTracks`. If format version is higher 349 | than 5.0, 1 blank byte is read. 350 | """ 351 | super().readTracks(song, trackCount, channels) 352 | self.skip(2 if self.versionTuple == (5, 0, 0) else 1) # Always 0 353 | 354 | def readTrack(self, track, channels): 355 | """Read track. 356 | 357 | If it's Guitar Pro 5.0 format and track is first then one blank 358 | byte is read. 359 | 360 | Then go track's flags. It presides the track's attributes: 361 | 362 | - *0x01*: drums track 363 | - *0x02*: 12 stringed guitar track 364 | - *0x04*: banjo track 365 | - *0x08*: track visibility 366 | - *0x10*: track is soloed 367 | - *0x20*: track is muted 368 | - *0x40*: RSE is enabled 369 | - *0x80*: show tuning in the header of the sheet. 370 | 371 | Flags are followed by: 372 | 373 | - Name: `String`. A 40 characters long string containing the 374 | track's name. 375 | 376 | - Number of strings: :ref:`int`. An integer equal to the number 377 | of strings of the track. 378 | 379 | - Tuning of the strings: `Table of integers`. The tuning of the 380 | strings is stored as a 7-integers table, the "Number of 381 | strings" first integers being really used. The strings are 382 | stored from the highest to the lowest. 383 | 384 | - Port: :ref:`int`. The number of the MIDI port used. 385 | 386 | - Channel. See :meth:`GP3File.readChannel`. 387 | 388 | - Number of frets: :ref:`int`. The number of frets of the 389 | instrument. 390 | 391 | - Height of the capo: :ref:`int`. The number of the fret on 392 | which a capo is set. If no capo is used, the value is 0. 393 | 394 | - Track's color. The track's displayed color in Guitar Pro. 395 | 396 | The properties are followed by second set of flags stored in a 397 | :ref:`short`. 398 | 399 | - *0x0001*: show tablature 400 | - *0x0002*: show standard notation 401 | - *0x0004*: chord diagrams are below standard notation 402 | - *0x0008*: show rhythm with tab 403 | - *0x0010*: force horizontal beams 404 | - *0x0020*: force channels 11 to 16 405 | - *0x0040*: diagram list on top of the score 406 | - *0x0080*: diagrams in the score 407 | - *0x0200*: auto let-ring 408 | - *0x0400*: auto brush 409 | - *0x0800*: extend rhythmic inside the tab 410 | 411 | Then follow: 412 | 413 | - Auto accentuation: :ref:`byte`. See 414 | :class:`guitarpro.models.Accentuation`. 415 | 416 | - MIDI bank: :ref:`byte`. 417 | 418 | - Track RSE. See :meth:`readTrackRSE`. 419 | """ 420 | if track.number == 1 or self.versionTuple == (5, 0, 0): 421 | # Always 0 422 | self.skip(1) 423 | flags1 = self.readByte() 424 | track.isPercussionTrack = bool(flags1 & 0x01) 425 | track.is12StringedGuitarTrack = bool(flags1 & 0x02) 426 | track.isBanjoTrack = bool(flags1 & 0x04) 427 | track.isVisible = bool(flags1 & 0x08) 428 | track.isSolo = bool(flags1 & 0x10) 429 | track.isMute = bool(flags1 & 0x20) 430 | track.useRSE = bool(flags1 & 0x40) 431 | track.indicateTuning = bool(flags1 & 0x80) 432 | track.name = self.readByteSizeString(40) 433 | stringCount = self.readInt() 434 | for i in range(7): 435 | iTuning = self.readInt() 436 | if stringCount > i: 437 | oString = gp.GuitarString(i + 1, iTuning) 438 | track.strings.append(oString) 439 | track.port = self.readInt() 440 | track.channel = self.readChannel(channels) 441 | if track.channel.channel == 9: 442 | track.isPercussionTrack = True 443 | track.fretCount = self.readInt() 444 | track.offset = self.readInt() 445 | track.color = self.readColor() 446 | 447 | flags2 = self.readShort() 448 | track.settings = gp.TrackSettings() 449 | track.settings.tablature = bool(flags2 & 0x0001) 450 | track.settings.notation = bool(flags2 & 0x0002) 451 | track.settings.diagramsAreBelow = bool(flags2 & 0x0004) 452 | track.settings.showRhythm = bool(flags2 & 0x0008) 453 | track.settings.forceHorizontal = bool(flags2 & 0x0010) 454 | track.settings.forceChannels = bool(flags2 & 0x0020) 455 | track.settings.diagramList = bool(flags2 & 0x0040) 456 | track.settings.diagramsInScore = bool(flags2 & 0x0080) 457 | # 0x0100: ??? 458 | track.settings.autoLetRing = bool(flags2 & 0x0200) 459 | track.settings.autoBrush = bool(flags2 & 0x0400) 460 | track.settings.extendRhythmic = bool(flags2 & 0x0800) 461 | 462 | track.rse = gp.TrackRSE() 463 | track.rse.autoAccentuation = gp.Accentuation(self.readByte()) 464 | track.channel.bank = self.readByte() 465 | self.readTrackRSE(track.rse) 466 | 467 | def readTrackRSE(self, trackRSE): 468 | """Read track RSE. 469 | 470 | In GuitarPro 5.1 track RSE is read as follows: 471 | 472 | - Humanize: :ref:`byte`. 473 | 474 | - Unknown space: 6 :ref:`Ints `. 475 | 476 | - RSE instrument. See :meth:`readRSEInstrument`. 477 | 478 | - 3-band track equalizer. See :meth:`readEqualizer`. 479 | 480 | - RSE instrument effect. See :meth:`readRSEInstrumentEffect`. 481 | """ 482 | trackRSE.humanize = self.readByte() 483 | self.readInt(3) # ??? 484 | self.skip(12) # ??? 485 | trackRSE.instrument = self.readRSEInstrument() 486 | if self.versionTuple > (5, 0, 0): 487 | trackRSE.equalizer = self.readEqualizer(4) 488 | self.readRSEInstrumentEffect(trackRSE.instrument) 489 | return trackRSE 490 | 491 | def readRSEInstrument(self): 492 | """Read RSE instrument. 493 | 494 | - MIDI instrument number: :ref:`int`. 495 | 496 | - Unknown :ref:`int`. 497 | 498 | - Sound bank: :ref:`int`. 499 | 500 | - Effect number: :ref:`int`. Vestige of Guitar Pro 5.0 format. 501 | """ 502 | instrument = gp.RSEInstrument() 503 | instrument.instrument = self.readInt() 504 | instrument.unknown = self.readInt() # ??? mostly 1 505 | instrument.soundBank = self.readInt() 506 | if self.versionTuple == (5, 0, 0): 507 | instrument.effectNumber = self.readShort() 508 | self.skip(1) 509 | else: 510 | instrument.effectNumber = self.readInt() 511 | return instrument 512 | 513 | def readRSEInstrumentEffect(self, rseInstrument): 514 | """Read RSE instrument effect name. 515 | 516 | This feature was introduced in Guitar Pro 5.1. 517 | 518 | - Effect name: :ref:`int-byte-size-string`. 519 | 520 | - Effect category: :ref:`int-byte-size-string`. 521 | """ 522 | if self.versionTuple > (5, 0, 0): 523 | effect = self.readIntByteSizeString() 524 | effectCategory = self.readIntByteSizeString() 525 | if rseInstrument is not None: 526 | rseInstrument.effect = effect 527 | rseInstrument.effectCategory = effectCategory 528 | return rseInstrument 529 | 530 | def readMeasure(self, measure): 531 | """Read measure. 532 | 533 | Guitar Pro 5 stores twice more measures compared to Guitar Pro 534 | 3. One measure consists of two sub-measures for each of two 535 | voices. 536 | 537 | Sub-measures are followed by a 538 | :class:`~guitarpro.models.LineBreak` stored in :ref:`byte`. 539 | """ 540 | start = measure.start 541 | for number, voice in enumerate(measure.voices[:gp.Measure.maxVoices]): 542 | self._currentVoiceNumber = number + 1 543 | self.readVoice(start, voice) 544 | self._currentVoiceNumber = None 545 | measure.lineBreak = gp.LineBreak(self.readByte(default=0)) 546 | 547 | def readBeat(self, start, voice): 548 | """Read beat. 549 | 550 | First, beat is read is in Guitar Pro 3 551 | :meth:`guitarpro.gp3.readBeat`. Then it is followed by set of 552 | flags stored in :ref:`short`. 553 | 554 | - *0x0001*: break beams 555 | - *0x0002*: direct beams down 556 | - *0x0004*: force beams 557 | - *0x0008*: direct beams up 558 | - *0x0010*: ottava (8va) 559 | - *0x0020*: ottava bassa (8vb) 560 | - *0x0040*: quindicesima (15ma) 561 | - *0x0100*: quindicesima bassa (15mb) 562 | - *0x0200*: start tuplet bracket here 563 | - *0x0400*: end tuplet bracket here 564 | - *0x0800*: break secondary beams 565 | - *0x1000*: break secondary tuplet 566 | - *0x2000*: force tuplet bracket 567 | 568 | - Break secondary beams: :ref:`byte`. Appears if flag at 569 | *0x0800* is set. Signifies how much beams should be broken. 570 | """ 571 | duration = super().readBeat(start, voice) 572 | beat = self.getBeat(voice, start) 573 | flags2 = self.readShort() 574 | if flags2 & 0x0010: 575 | beat.octave = gp.Octave.ottava 576 | if flags2 & 0x0020: 577 | beat.octave = gp.Octave.ottavaBassa 578 | if flags2 & 0x0040: 579 | beat.octave = gp.Octave.quindicesima 580 | if flags2 & 0x0100: 581 | beat.octave = gp.Octave.quindicesimaBassa 582 | display = gp.BeatDisplay() 583 | display.breakBeam = bool(flags2 & 0x0001) 584 | display.forceBeam = bool(flags2 & 0x0004) 585 | display.forceBracket = bool(flags2 & 0x2000) 586 | display.breakSecondaryTuplet = bool(flags2 & 0x1000) 587 | if flags2 & 0x0002: 588 | display.beamDirection = gp.VoiceDirection.down 589 | if flags2 & 0x0008: 590 | display.beamDirection = gp.VoiceDirection.up 591 | if flags2 & 0x0200: 592 | display.tupletBracket = gp.TupletBracket.start 593 | if flags2 & 0x0400: 594 | display.tupletBracket = gp.TupletBracket.end 595 | if flags2 & 0x0800: 596 | display.breakSecondary = self.readByte() 597 | beat.display = display 598 | return duration 599 | 600 | def readBeatStroke(self): 601 | """Read beat stroke. 602 | 603 | Beat stroke consists of two :ref:`Bytes ` which correspond 604 | to stroke down and stroke up speed. See 605 | :class:`guitarpro.models.BeatStroke` for value mapping. 606 | """ 607 | stroke = super().readBeatStroke() 608 | return stroke.swapDirection() 609 | 610 | def readMixTableChange(self, measure): 611 | """Read mix table change. 612 | 613 | Mix table change was modified to support RSE instruments. It is 614 | read as in Guitar Pro 3 and is followed by: 615 | 616 | - Wah effect. See :meth:`readWahEffect`. 617 | 618 | - RSE instrument effect. See :meth:`readRSEInstrumentEffect`. 619 | """ 620 | tableChange = super(gp4.GP4File, self).readMixTableChange(measure) 621 | flags = self.readMixTableChangeFlags(tableChange) 622 | tableChange.wah = self.readWahEffect(flags) 623 | self.readRSEInstrumentEffect(tableChange.rse) 624 | return tableChange 625 | 626 | def readMixTableChangeValues(self, tableChange, measure): 627 | """Read mix table change values. 628 | 629 | Mix table change values consist of: 630 | 631 | - Instrument: :ref:`signed-byte`. 632 | 633 | - RSE instrument. See `readRSEInstrument`. 634 | 635 | - Volume: :ref:`signed-byte`. 636 | 637 | - Balance: :ref:`signed-byte`. 638 | 639 | - Chorus: :ref:`signed-byte`. 640 | 641 | - Reverb: :ref:`signed-byte`. 642 | 643 | - Phaser: :ref:`signed-byte`. 644 | 645 | - Tremolo: :ref:`signed-byte`. 646 | 647 | - Tempo name: :ref:`int-byte-size-string`. 648 | 649 | - Tempo: :ref:`int`. 650 | 651 | If the value is -1 then corresponding parameter hasn't changed. 652 | """ 653 | instrument = self.readSignedByte() 654 | rse = self.readRSEInstrument() 655 | if self.versionTuple == (5, 0, 0): 656 | self.skip(1) 657 | volume = self.readSignedByte() 658 | balance = self.readSignedByte() 659 | chorus = self.readSignedByte() 660 | reverb = self.readSignedByte() 661 | phaser = self.readSignedByte() 662 | tremolo = self.readSignedByte() 663 | tempoName = self.readIntByteSizeString() 664 | tempo = self.readInt() 665 | if instrument >= 0: 666 | tableChange.instrument = gp.MixTableItem(instrument) 667 | tableChange.rse = rse 668 | if volume >= 0: 669 | tableChange.volume = gp.MixTableItem(volume) 670 | if balance >= 0: 671 | tableChange.balance = gp.MixTableItem(balance) 672 | if chorus >= 0: 673 | tableChange.chorus = gp.MixTableItem(chorus) 674 | if reverb >= 0: 675 | tableChange.reverb = gp.MixTableItem(reverb) 676 | if phaser >= 0: 677 | tableChange.phaser = gp.MixTableItem(phaser) 678 | if tremolo >= 0: 679 | tableChange.tremolo = gp.MixTableItem(tremolo) 680 | if tempo >= 0: 681 | tableChange.tempo = gp.MixTableItem(tempo) 682 | tableChange.tempoName = tempoName 683 | 684 | def readMixTableChangeDurations(self, tableChange): 685 | """Read mix table change durations. 686 | 687 | Durations are read for each non-null 688 | :class:`~guitarpro.models.MixTableItem`. Durations are encoded 689 | in :ref:`signed-byte`. 690 | 691 | If tempo did change, then one :ref:`bool` is read. If it's true, 692 | then tempo change won't be displayed on the score. 693 | """ 694 | if tableChange.volume is not None: 695 | tableChange.volume.duration = self.readSignedByte() 696 | if tableChange.balance is not None: 697 | tableChange.balance.duration = self.readSignedByte() 698 | if tableChange.chorus is not None: 699 | tableChange.chorus.duration = self.readSignedByte() 700 | if tableChange.reverb is not None: 701 | tableChange.reverb.duration = self.readSignedByte() 702 | if tableChange.phaser is not None: 703 | tableChange.phaser.duration = self.readSignedByte() 704 | if tableChange.tremolo is not None: 705 | tableChange.tremolo.duration = self.readSignedByte() 706 | if tableChange.tempo is not None: 707 | tableChange.tempo.duration = self.readSignedByte() 708 | tableChange.hideTempo = self.versionTuple > (5, 0, 0) and self.readBool() 709 | 710 | def readMixTableChangeFlags(self, tableChange): 711 | """Read mix table change flags. 712 | 713 | Mix table change flags are read as in Guitar Pro 4 714 | :meth:`guitarpro.gp4.readMixTableChangeFlags`, with one 715 | additional flag: 716 | 717 | - *0x40*: use RSE 718 | - *0x80*: show wah-wah 719 | """ 720 | flags = super().readMixTableChangeFlags(tableChange) 721 | tableChange.useRSE = bool(flags & 0x40) 722 | return flags 723 | 724 | def readWahEffect(self, flags): 725 | """Read wah-wah. 726 | 727 | - Wah value: :ref:`signed-byte`. See 728 | :class:`guitarpro.models.WahEffect` for value mapping. 729 | """ 730 | return gp.WahEffect(value=self.readSignedByte(), 731 | display=bool(flags & 0x80)) 732 | 733 | def readNote(self, note, guitarString, track): 734 | """Read note. 735 | 736 | The first byte is note flags: 737 | 738 | - *0x01*: duration percent 739 | - *0x02*: heavy accentuated note 740 | - *0x04*: ghost note 741 | - *0x08*: presence of note effects 742 | - *0x10*: dynamics 743 | - *0x20*: fret 744 | - *0x40*: accentuated note 745 | - *0x80*: right hand or left hand fingering 746 | 747 | Flags are followed by: 748 | 749 | - Note type: :ref:`byte`. Note is normal if values is 1, tied if 750 | value is 2, dead if value is 3. 751 | 752 | - Note dynamics: :ref:`signed-byte`. See 753 | :meth:`unpackVelocity`. 754 | 755 | - Fret number: :ref:`signed-byte`. If flag at *0x20* is set then 756 | read fret number. 757 | 758 | - Fingering: 2 :ref:`SignedBytes `. See 759 | :class:`guitarpro.models.Fingering`. 760 | 761 | - Duration percent: :ref:`double`. 762 | 763 | - Second set of flags: :ref:`byte`. 764 | 765 | - *0x02*: swap accidentals. 766 | 767 | - Note effects. See :meth:`guitarpro.gp4.readNoteEffects`. 768 | """ 769 | flags = self.readByte() 770 | note.string = guitarString.number 771 | note.effect.heavyAccentuatedNote = bool(flags & 0x02) 772 | note.effect.ghostNote = bool(flags & 0x04) 773 | note.effect.accentuatedNote = bool(flags & 0x40) 774 | if flags & 0x20: 775 | note.type = gp.NoteType(self.readByte()) 776 | if flags & 0x10: 777 | dyn = self.readSignedByte() 778 | note.velocity = self.unpackVelocity(dyn) 779 | if flags & 0x20: 780 | fret = self.readSignedByte() 781 | if note.type == gp.NoteType.tie: 782 | value = self.getTiedNoteValue(guitarString.number, track) 783 | else: 784 | value = fret 785 | note.value = value if 0 <= value < 100 else 0 786 | if flags & 0x80: 787 | note.effect.leftHandFinger = gp.Fingering(self.readSignedByte()) 788 | note.effect.rightHandFinger = gp.Fingering(self.readSignedByte()) 789 | if flags & 0x01: 790 | note.durationPercent = self.readDouble() 791 | flags2 = self.readByte() 792 | note.swapAccidentals = bool(flags2 & 0x02) 793 | if flags & 0x08: 794 | note.effect = self.readNoteEffects(note) 795 | return note 796 | 797 | def readGrace(self): 798 | """Read grace note effect. 799 | 800 | - Fret: :ref:`signed-byte`. Number of fret. 801 | 802 | - Dynamic: :ref:`byte`. Dynamic of a grace note, as in 803 | :attr:`guitarpro.models.Note.velocity`. 804 | 805 | - Transition: :ref:`byte`. See 806 | :class:`guitarpro.models.GraceEffectTransition`. 807 | 808 | - Duration: :ref:`byte`. Values are: 809 | 810 | - *1*: Thirty-second note. 811 | - *2*: Twenty-fourth note. 812 | - *3*: Sixteenth note. 813 | 814 | - Flags: :ref:`byte`. 815 | 816 | - *0x01*: grace note is muted (dead) 817 | - *0x02*: grace note is on beat 818 | """ 819 | grace = gp.GraceEffect() 820 | grace.fret = self.readByte() 821 | grace.velocity = self.unpackVelocity(self.readByte()) 822 | grace.transition = gp.GraceEffectTransition(self.readByte()) 823 | grace.duration = 1 << (7 - self.readByte()) 824 | flags = self.readByte() 825 | grace.isDead = bool(flags & 0x01) 826 | grace.isOnBeat = bool(flags & 0x02) 827 | return grace 828 | 829 | def readSlides(self): 830 | """Read slides. 831 | 832 | First :ref:`byte` stores slide types: 833 | 834 | - *0x01*: shift slide 835 | - *0x02*: legato slide 836 | - *0x04*: slide out downwards 837 | - *0x08*: slide out upwards 838 | - *0x10*: slide into from below 839 | - *0x20*: slide into from above 840 | """ 841 | slideType = self.readByte() 842 | slides = [] 843 | if slideType & 0x01: 844 | slides.append(gp.SlideType.shiftSlideTo) 845 | if slideType & 0x02: 846 | slides.append(gp.SlideType.legatoSlideTo) 847 | if slideType & 0x04: 848 | slides.append(gp.SlideType.outDownwards) 849 | if slideType & 0x08: 850 | slides.append(gp.SlideType.outUpwards) 851 | if slideType & 0x10: 852 | slides.append(gp.SlideType.intoFromBelow) 853 | if slideType & 0x20: 854 | slides.append(gp.SlideType.intoFromAbove) 855 | return slides 856 | 857 | def readHarmonic(self, note): 858 | """Read harmonic. 859 | 860 | First :ref:`byte` is harmonic type: 861 | 862 | - *1*: natural harmonic 863 | - *2*: artificial harmonic 864 | - *3*: tapped harmonic 865 | - *4*: pinch harmonic 866 | - *5*: semi-harmonic 867 | 868 | In case harmonic types is artificial, following data is read: 869 | 870 | - Note: :ref:`byte`. 871 | - Accidental: :ref:`signed-byte`. 872 | - Octave: :ref:`byte`. 873 | 874 | If harmonic type is tapped: 875 | 876 | - Fret: :ref:`byte`. 877 | """ 878 | harmonicType = self.readSignedByte() 879 | if harmonicType == 1: 880 | harmonic = gp.NaturalHarmonic() 881 | elif harmonicType == 2: 882 | # C = 0, D = 2, E = 4, F = 5... 883 | # b = -1, # = 1 884 | # loco = 0, 8va = 1, 15ma = 2 885 | semitone = self.readByte() 886 | accidental = self.readSignedByte() 887 | pitchClass = gp.PitchClass(semitone, accidental) 888 | octave = gp.Octave(self.readByte()) 889 | harmonic = gp.ArtificialHarmonic(pitchClass, octave) 890 | elif harmonicType == 3: 891 | fret = self.readByte() 892 | harmonic = gp.TappedHarmonic(fret) 893 | elif harmonicType == 4: 894 | harmonic = gp.PinchHarmonic() 895 | elif harmonicType == 5: 896 | harmonic = gp.SemiHarmonic() 897 | return harmonic 898 | 899 | # Writing 900 | # ======= 901 | 902 | def writeSong(self, song): 903 | self.writeVersion() 904 | self.writeClipboard(song.clipboard) 905 | 906 | self.writeInfo(song) 907 | self.writeLyrics(song.lyrics) 908 | self.writeRSEMasterEffect(song.masterEffect) 909 | self.writePageSetup(song.pageSetup) 910 | 911 | self.writeIntByteSizeString(song.tempoName) 912 | self.writeInt(song.tempo) 913 | 914 | if self.versionTuple > (5, 0, 0): 915 | self.writeBool(song.hideTempo) 916 | 917 | self.writeSignedByte(song.key.value[0]) 918 | self.writeInt(0) # octave 919 | 920 | self.writeMidiChannels(song.tracks) 921 | 922 | self.writeDirections(song.measureHeaders) 923 | self.writeMasterReverb(song.masterEffect) 924 | 925 | measureCount = len(song.tracks[0].measures) 926 | trackCount = len(song.tracks) 927 | self.writeInt(measureCount) 928 | self.writeInt(trackCount) 929 | 930 | with self.annotateErrors('writing'): 931 | self.writeMeasureHeaders(song.tracks[0].measures) 932 | self.writeTracks(song.tracks) 933 | self.writeMeasures(song.tracks) 934 | 935 | def writeClipboard(self, clipboard): 936 | if clipboard is None: 937 | return 938 | super().writeClipboard(clipboard) 939 | self.writeInt(clipboard.startBeat) 940 | self.writeInt(clipboard.stopBeat) 941 | self.writeInt(int(clipboard.subBarCopy)) 942 | 943 | def writeInfo(self, song): 944 | self.writeIntByteSizeString(song.title) 945 | self.writeIntByteSizeString(song.subtitle) 946 | self.writeIntByteSizeString(song.artist) 947 | self.writeIntByteSizeString(song.album) 948 | self.writeIntByteSizeString(song.words) 949 | self.writeIntByteSizeString(song.music) 950 | self.writeIntByteSizeString(song.copyright) 951 | self.writeIntByteSizeString(song.tab) 952 | self.writeIntByteSizeString(song.instructions) 953 | 954 | self.writeInt(len(song.notice)) 955 | for line in song.notice: 956 | self.writeIntByteSizeString(line) 957 | 958 | def writeRSEMasterEffect(self, masterEffect): 959 | if self.versionTuple > (5, 0, 0): 960 | masterEffect.volume = masterEffect.volume or 100 961 | masterEffect.reverb = masterEffect.reverb or 0 962 | masterEffect.equalizer = masterEffect.equalizer or gp.RSEEqualizer(knobs=[0] * 10, gain=0) 963 | self.writeInt(masterEffect.volume) 964 | self.writeInt(0) 965 | self.writeEqualizer(masterEffect.equalizer) 966 | 967 | def writeEqualizer(self, equalizer): 968 | for knob in equalizer.knobs: 969 | self.writeSignedByte(self.packVolumeValue(knob)) 970 | self.writeSignedByte(self.packVolumeValue(equalizer.gain)) 971 | 972 | def packVolumeValue(self, value): 973 | return int(-round(value, 1) * 10) 974 | 975 | def writePageSetup(self, setup): 976 | self.writeInt(setup.pageSize.x) 977 | self.writeInt(setup.pageSize.y) 978 | 979 | self.writeInt(setup.pageMargin.left) 980 | self.writeInt(setup.pageMargin.right) 981 | self.writeInt(setup.pageMargin.top) 982 | self.writeInt(setup.pageMargin.bottom) 983 | self.writeInt(int(setup.scoreSizeProportion * 100)) 984 | 985 | self.writeByte(setup.headerAndFooter & 0xff) 986 | 987 | flags2 = 0x00 988 | if setup.headerAndFooter and gp.HeaderFooterElements.pageNumber != 0: 989 | flags2 |= 0x01 990 | self.writeByte(flags2) 991 | 992 | self.writeIntByteSizeString(setup.title) 993 | self.writeIntByteSizeString(setup.subtitle) 994 | self.writeIntByteSizeString(setup.artist) 995 | self.writeIntByteSizeString(setup.album) 996 | self.writeIntByteSizeString(setup.words) 997 | self.writeIntByteSizeString(setup.music) 998 | self.writeIntByteSizeString(setup.wordsAndMusic) 999 | copyrighta, copyrightb = setup.copyright.split('\n', 1) 1000 | self.writeIntByteSizeString(copyrighta) 1001 | self.writeIntByteSizeString(copyrightb) 1002 | self.writeIntByteSizeString(setup.pageNumber) 1003 | 1004 | def writeDirections(self, measureHeaders): 1005 | order = ['Coda', 1006 | 'Double Coda', 1007 | 'Segno', 1008 | 'Segno Segno', 1009 | 'Fine', 1010 | 'Da Capo', 1011 | 'Da Capo al Coda', 1012 | 'Da Capo al Double Coda', 1013 | 'Da Capo al Fine', 1014 | 'Da Segno', 1015 | 'Da Segno al Coda', 1016 | 'Da Segno al Double Coda', 1017 | 'Da Segno al Fine', 1018 | 'Da Segno Segno', 1019 | 'Da Segno Segno al Coda', 1020 | 'Da Segno Segno al Double Coda', 1021 | 'Da Segno Segno al Fine', 1022 | 'Da Coda', 1023 | 'Da Double Coda'] 1024 | 1025 | signs = {} 1026 | for number, header in enumerate(measureHeaders, start=1): 1027 | if header.direction is not None: 1028 | signs[header.direction.name] = number 1029 | if header.fromDirection is not None: 1030 | signs[header.fromDirection.name] = number 1031 | 1032 | for name in order: 1033 | self.writeShort(signs.get(name, -1)) 1034 | 1035 | def writeMasterReverb(self, masterEffect): 1036 | if masterEffect is not None: 1037 | self.writeInt(masterEffect.reverb) 1038 | else: 1039 | self.writeInt(0) 1040 | 1041 | def writeMeasureHeader(self, header, previous=None): 1042 | flags = self.packMeasureHeaderFlags(header, previous) 1043 | if previous is not None: 1044 | self.placeholder(1) 1045 | self.writeMeasureHeaderValues(header, flags) 1046 | 1047 | def packMeasureHeaderFlags(self, header, previous=None): 1048 | flags = super().packMeasureHeaderFlags(header, previous) 1049 | if previous is not None: 1050 | if header.timeSignature.beams != previous.timeSignature.beams: 1051 | flags |= 0x03 1052 | return flags 1053 | 1054 | def writeMeasureHeaderValues(self, header, flags): 1055 | header = attr.evolve(header, repeatClose=header.repeatClose+1) 1056 | self.writeByte(flags) 1057 | if flags & 0x01: 1058 | self.writeSignedByte(header.timeSignature.numerator) 1059 | if flags & 0x02: 1060 | self.writeSignedByte(header.timeSignature.denominator.value) 1061 | if flags & 0x08: 1062 | self.writeSignedByte(header.repeatClose) 1063 | if flags & 0x20: 1064 | self.writeMarker(header.marker) 1065 | if flags & 0x40: 1066 | self.writeSignedByte(header.keySignature.value[0]) 1067 | self.writeSignedByte(header.keySignature.value[1]) 1068 | if flags & 0x10: 1069 | self.writeRepeatAlternative(header.repeatAlternative) 1070 | if flags & 0x03: 1071 | for beam in header.timeSignature.beams: 1072 | self.writeByte(beam) 1073 | if flags & 0x10 == 0: 1074 | self.placeholder(1) 1075 | self.writeByte(header.tripletFeel.value) 1076 | 1077 | def writeRepeatAlternative(self, repeatAlternative): 1078 | self.writeByte(repeatAlternative & 255) 1079 | 1080 | def writeTracks(self, tracks): 1081 | super().writeTracks(tracks) 1082 | self.placeholder(2 if self.versionTuple == (5, 0, 0) else 1) 1083 | 1084 | def writeTrack(self, track, number): 1085 | if number == 1 or self.versionTuple == (5, 0, 0): 1086 | self.placeholder(1) 1087 | 1088 | flags1 = 0x00 1089 | if track.isPercussionTrack: 1090 | flags1 |= 0x01 1091 | if track.is12StringedGuitarTrack: 1092 | flags1 |= 0x02 1093 | if track.isBanjoTrack: 1094 | flags1 |= 0x04 1095 | if track.isVisible: 1096 | flags1 |= 0x08 1097 | if track.isSolo: 1098 | flags1 |= 0x10 1099 | if track.isMute: 1100 | flags1 |= 0x20 1101 | if track.useRSE: 1102 | flags1 |= 0x40 1103 | if track.indicateTuning: 1104 | flags1 |= 0x80 1105 | 1106 | self.writeByte(flags1) 1107 | 1108 | self.writeByteSizeString(track.name, 40) 1109 | self.writeInt(len(track.strings)) 1110 | for i in range(7): 1111 | if i < len(track.strings): 1112 | tuning = track.strings[i].value 1113 | else: 1114 | tuning = 0 1115 | self.writeInt(tuning) 1116 | self.writeInt(track.port) 1117 | self.writeChannel(track) 1118 | self.writeInt(track.fretCount) 1119 | self.writeInt(track.offset) 1120 | self.writeColor(track.color) 1121 | 1122 | flags2 = 0x0000 1123 | if track.settings.tablature: 1124 | flags2 |= 0x0001 1125 | if track.settings.notation: 1126 | flags2 |= 0x0002 1127 | if track.settings.diagramsAreBelow: 1128 | flags2 |= 0x0004 1129 | if track.settings.showRhythm: 1130 | flags2 |= 0x0008 1131 | if track.settings.forceHorizontal: 1132 | flags2 |= 0x0010 1133 | if track.settings.forceChannels: 1134 | flags2 |= 0x0020 1135 | if track.settings.diagramList: 1136 | flags2 |= 0x0040 1137 | if track.settings.diagramsInScore: 1138 | flags2 |= 0x0080 1139 | if track.settings.autoLetRing: 1140 | flags2 |= 0x0200 1141 | if track.settings.autoBrush: 1142 | flags2 |= 0x0400 1143 | if track.settings.extendRhythmic: 1144 | flags2 |= 0x0800 1145 | self.writeShort(flags2) 1146 | 1147 | if track.rse is not None and track.rse.autoAccentuation is not None: 1148 | self.writeByte(track.rse.autoAccentuation.value) 1149 | else: 1150 | self.writeByte(0) 1151 | self.writeByte(track.channel.bank) 1152 | 1153 | self.writeTrackRSE(track.rse) 1154 | 1155 | def writeTrackRSE(self, trackRSE): 1156 | self.writeByte(trackRSE.humanize) 1157 | self.writeInt(0) 1158 | self.writeInt(0) 1159 | self.writeInt(100) 1160 | self.placeholder(12) 1161 | self.writeRSEInstrument(trackRSE.instrument) 1162 | if self.versionTuple > (5, 0, 0): 1163 | self.writeEqualizer(trackRSE.equalizer) 1164 | self.writeRSEInstrumentEffect(trackRSE.instrument) 1165 | 1166 | def writeRSEInstrument(self, instrument): 1167 | self.writeInt(instrument.instrument) 1168 | self.writeInt(instrument.unknown) 1169 | self.writeInt(instrument.soundBank) 1170 | if self.versionTuple == (5, 0, 0): 1171 | self.writeShort(instrument.effectNumber) 1172 | self.placeholder(1) 1173 | else: 1174 | self.writeInt(instrument.effectNumber) 1175 | 1176 | def writeRSEInstrumentEffect(self, instrument): 1177 | if self.versionTuple > (5, 0, 0): 1178 | self.writeIntByteSizeString(instrument.effect) 1179 | self.writeIntByteSizeString(instrument.effectCategory) 1180 | 1181 | def writeMeasure(self, measure): 1182 | for number, voice in enumerate(measure.voices[:gp.Measure.maxVoices]): 1183 | self._currentVoiceNumber = number + 1 1184 | self.writeVoice(voice) 1185 | self._currentVoiceNumber = None 1186 | self.writeByte(measure.lineBreak.value) 1187 | 1188 | def writeBeat(self, beat): 1189 | super().writeBeat(beat) 1190 | flags2 = 0x0000 1191 | if beat.display.breakBeam: 1192 | flags2 |= 0x0001 1193 | if beat.display.beamDirection == gp.VoiceDirection.down: 1194 | flags2 |= 0x0002 1195 | if beat.display.forceBeam: 1196 | flags2 |= 0x0004 1197 | if beat.display.beamDirection == gp.VoiceDirection.up: 1198 | flags2 |= 0x0008 1199 | if beat.octave == gp.Octave.ottava: 1200 | flags2 |= 0x0010 1201 | if beat.octave == gp.Octave.ottavaBassa: 1202 | flags2 |= 0x0020 1203 | if beat.octave == gp.Octave.quindicesima: 1204 | flags2 |= 0x0040 1205 | if beat.octave == gp.Octave.quindicesimaBassa: 1206 | flags2 |= 0x0100 1207 | if beat.display.tupletBracket == gp.TupletBracket.start: 1208 | flags2 |= 0x0200 1209 | if beat.display.tupletBracket == gp.TupletBracket.end: 1210 | flags2 |= 0x0400 1211 | if beat.display.breakSecondary: 1212 | flags2 |= 0x0800 1213 | if beat.display.breakSecondaryTuplet: 1214 | flags2 |= 0x1000 1215 | if beat.display.forceBracket: 1216 | flags2 |= 0x2000 1217 | self.writeShort(flags2) 1218 | if flags2 & 0x0800: 1219 | self.writeByte(beat.display.breakSecondary) 1220 | 1221 | def writeBeatStroke(self, stroke): 1222 | super().writeBeatStroke(stroke.swapDirection()) 1223 | 1224 | def writeMixTableChange(self, tableChange): 1225 | super(gp4.GP4File, self).writeMixTableChange(tableChange) 1226 | self.writeMixTableChangeFlags(tableChange) 1227 | self.writeWahEffect(tableChange.wah) 1228 | self.writeRSEInstrumentEffect(tableChange.rse) 1229 | 1230 | def writeMixTableChangeValues(self, tableChange): 1231 | self.writeSignedByte(tableChange.instrument.value 1232 | if tableChange.instrument is not None else -1) 1233 | self.writeRSEInstrument(tableChange.rse) 1234 | if self.versionTuple == (5, 0, 0): 1235 | self.placeholder(1) 1236 | self.writeSignedByte(tableChange.volume.value 1237 | if tableChange.volume is not None else -1) 1238 | self.writeSignedByte(tableChange.balance.value 1239 | if tableChange.balance is not None else -1) 1240 | self.writeSignedByte(tableChange.chorus.value 1241 | if tableChange.chorus is not None else -1) 1242 | self.writeSignedByte(tableChange.reverb.value 1243 | if tableChange.reverb is not None else -1) 1244 | self.writeSignedByte(tableChange.phaser.value 1245 | if tableChange.phaser is not None else -1) 1246 | self.writeSignedByte(tableChange.tremolo.value 1247 | if tableChange.tremolo is not None else -1) 1248 | self.writeIntByteSizeString(tableChange.tempoName) 1249 | self.writeInt(tableChange.tempo.value 1250 | if tableChange.tempo is not None else -1) 1251 | 1252 | def writeMixTableChangeDurations(self, tableChange): 1253 | if tableChange.volume is not None: 1254 | self.writeSignedByte(tableChange.volume.duration) 1255 | if tableChange.balance is not None: 1256 | self.writeSignedByte(tableChange.balance.duration) 1257 | if tableChange.chorus is not None: 1258 | self.writeSignedByte(tableChange.chorus.duration) 1259 | if tableChange.reverb is not None: 1260 | self.writeSignedByte(tableChange.reverb.duration) 1261 | if tableChange.phaser is not None: 1262 | self.writeSignedByte(tableChange.phaser.duration) 1263 | if tableChange.tremolo is not None: 1264 | self.writeSignedByte(tableChange.tremolo.duration) 1265 | if tableChange.tempo is not None: 1266 | self.writeSignedByte(tableChange.tempo.duration) 1267 | if self.versionTuple > (5, 0, 0): 1268 | self.writeBool(tableChange.hideTempo) 1269 | 1270 | def writeMixTableChangeFlags(self, tableChange): 1271 | flags = 0x00 1272 | if tableChange.volume is not None and tableChange.volume.allTracks: 1273 | flags |= 0x01 1274 | if tableChange.balance is not None and tableChange.balance.allTracks: 1275 | flags |= 0x02 1276 | if tableChange.chorus is not None and tableChange.chorus.allTracks: 1277 | flags |= 0x04 1278 | if tableChange.reverb is not None and tableChange.reverb.allTracks: 1279 | flags |= 0x08 1280 | if tableChange.phaser is not None and tableChange.phaser.allTracks: 1281 | flags |= 0x10 1282 | if tableChange.tremolo is not None and tableChange.tremolo.allTracks: 1283 | flags |= 0x20 1284 | if tableChange.useRSE: 1285 | flags |= 0x40 1286 | if tableChange.wah is not None and tableChange.wah.display: 1287 | flags |= 0x80 1288 | self.writeByte(flags) 1289 | 1290 | def writeWahEffect(self, wah): 1291 | if wah is not None: 1292 | self.writeSignedByte(wah.value) 1293 | else: 1294 | self.writeSignedByte(gp.WahEffect.none.value) 1295 | 1296 | def writeNote(self, note): 1297 | flags = self.packNoteFlags(note) 1298 | self.writeByte(flags) 1299 | if flags & 0x20: 1300 | self.writeByte(self.getEnumValue(note.type)) 1301 | if flags & 0x10: 1302 | value = self.packVelocity(note.velocity) 1303 | self.writeSignedByte(value) 1304 | if flags & 0x20: 1305 | fret = note.value if note.type != gp.NoteType.tie else 0 1306 | self.writeSignedByte(fret) 1307 | if flags & 0x80: 1308 | self.writeSignedByte(self.getEnumValue(note.effect.leftHandFinger)) 1309 | self.writeSignedByte(self.getEnumValue(note.effect.rightHandFinger)) 1310 | if flags & 0x01: 1311 | self.writeDouble(note.durationPercent) 1312 | flags2 = 0x00 1313 | if note.swapAccidentals: 1314 | flags2 |= 0x02 1315 | self.writeByte(flags2) 1316 | if flags & 0x08: 1317 | self.writeNoteEffects(note) 1318 | 1319 | def packNoteFlags(self, note): 1320 | flags = super().packNoteFlags(note) 1321 | if abs(note.durationPercent - 1.0) >= 1e-3: 1322 | flags |= 0x01 1323 | return flags 1324 | 1325 | def writeGrace(self, grace): 1326 | self.writeByte(grace.fret) 1327 | self.writeByte(self.packVelocity(grace.velocity)) 1328 | self.writeByte(grace.transition.value) 1329 | self.writeByte(8 - grace.duration.bit_length()) 1330 | flags = 0x00 1331 | if grace.isDead: 1332 | flags |= 0x01 1333 | if grace.isOnBeat: 1334 | flags |= 0x02 1335 | self.writeByte(flags) 1336 | 1337 | def writeSlides(self, slides): 1338 | slideType = 0 1339 | for slide in slides: 1340 | if slide == gp.SlideType.shiftSlideTo: 1341 | slideType |= 0x01 1342 | elif slide == gp.SlideType.legatoSlideTo: 1343 | slideType |= 0x02 1344 | elif slide == gp.SlideType.outDownwards: 1345 | slideType |= 0x04 1346 | elif slide == gp.SlideType.outUpwards: 1347 | slideType |= 0x08 1348 | elif slide == gp.SlideType.intoFromBelow: 1349 | slideType |= 0x10 1350 | elif slide == gp.SlideType.intoFromAbove: 1351 | slideType |= 0x20 1352 | self.writeByte(slideType) 1353 | 1354 | def writeHarmonic(self, note, harmonic): 1355 | self.writeSignedByte(harmonic.type) 1356 | if isinstance(harmonic, gp.ArtificialHarmonic): 1357 | if not harmonic.pitch or not harmonic.octave: 1358 | harmonic.pitch = gp.PitchClass(note.realValue % 12) 1359 | harmonic.octave = gp.Octave.ottava 1360 | self.writeByte(harmonic.pitch.just) 1361 | self.writeSignedByte(harmonic.pitch.accidental) 1362 | self.writeByte(harmonic.octave.value) 1363 | elif isinstance(harmonic, gp.TappedHarmonic): 1364 | self.writeByte(harmonic.fret) 1365 | -------------------------------------------------------------------------------- /guitarpro/io.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Optional 3 | 4 | from .iobase import GPFileBase 5 | from .gp3 import GP3File 6 | from .gp4 import GP4File 7 | from .gp5 import GP5File 8 | from .models import GPException, Song 9 | 10 | __all__ = ('parse', 'write') 11 | 12 | _GPFILES = { 13 | 'FICHIER GUITAR PRO v3.00': ((3, 0, 0), GP3File), 14 | 15 | 'FICHIER GUITAR PRO v4.00': ((4, 0, 0), GP4File), 16 | 'FICHIER GUITAR PRO v4.06': ((4, 0, 6), GP4File), 17 | 'FICHIER GUITAR PRO L4.06': ((4, 0, 6), GP4File), 18 | 'CLIPBOARD GUITAR PRO 4.0 [c6]': ((4, 0, 6), GP4File), 19 | 20 | 'FICHIER GUITAR PRO v5.00': ((5, 0, 0), GP5File), 21 | 'FICHIER GUITAR PRO v5.10': ((5, 1, 0), GP5File), 22 | 'CLIPBOARD GP 5.0': ((5, 0, 0), GP5File), 23 | 'CLIPBOARD GP 5.1': ((5, 1, 0), GP5File), 24 | 'CLIPBOARD GP 5.2': ((5, 2, 0), GP5File), 25 | } 26 | 27 | _VERSIONS = { 28 | # (versionTuple, isClipboard): versionString, 29 | ((3, 0, 0), False): 'FICHIER GUITAR PRO v3.00', 30 | 31 | ((4, 0, 0), False): 'FICHIER GUITAR PRO v4.00', 32 | ((4, 0, 6), False): 'FICHIER GUITAR PRO v4.06', 33 | ((4, 0, 6), True): 'CLIPBOARD GUITAR PRO 4.0 [c6]', 34 | 35 | ((5, 0, 0), False): 'FICHIER GUITAR PRO v5.00', 36 | ((5, 1, 0), False): 'FICHIER GUITAR PRO v5.10', 37 | ((5, 2, 0), False): 'FICHIER GUITAR PRO v5.10', # sic 38 | ((5, 0, 0), True): 'CLIPBOARD GP 5.0', 39 | ((5, 1, 0), True): 'CLIPBOARD GP 5.1', 40 | ((5, 2, 0), True): 'CLIPBOARD GP 5.2', 41 | } 42 | 43 | _EXT_VERSIONS = { 44 | 'gp3': (3, 0, 0), 45 | 'gp4': (4, 0, 6), 46 | 'gp5': (5, 1, 0), 47 | 'tmp': (5, 2, 0), 48 | } 49 | 50 | 51 | def parse(stream, encoding='cp1252') -> Song: 52 | """Open a GP file and read its contents. 53 | 54 | :param stream: path to a GP file or file-like object. 55 | :param encoding: decode strings in tablature using this charset. 56 | Given encoding must be an 8-bit charset. 57 | """ 58 | gpfile, shouldClose = _open(None, stream, 'rb', encoding=encoding) 59 | try: 60 | return gpfile.readSong() 61 | finally: 62 | if shouldClose: 63 | gpfile.close() 64 | 65 | 66 | def write(song: Song, stream, version: Optional[tuple] = None, encoding='cp1252'): 67 | """Write a song into GP file. 68 | 69 | :param song: a song to write. 70 | :param stream: path to save GP file or file-like object. 71 | :param version: explicitly set version of GP file to save, e.g. 72 | ``(5, 1, 0)``. 73 | :param encoding: encode strings into given 8-bit charset. 74 | """ 75 | gpfile, shouldClose = _open(song, stream, 'wb', version=version, encoding=encoding) 76 | try: 77 | gpfile.writeSong(song) 78 | finally: 79 | if shouldClose: 80 | gpfile.close() 81 | 82 | 83 | def _open(song, stream, mode='rb', version=None, encoding=None): 84 | """Open a GP file path for reading or writing.""" 85 | if mode not in ('rb', 'wb'): 86 | raise ValueError(f"cannot read or write unless in binary mode, not '{mode}'") 87 | 88 | shouldClose = False 89 | if isinstance(stream, (str, bytes, os.PathLike)): 90 | fp = open(stream, mode) 91 | shouldClose = True 92 | filename = stream 93 | else: 94 | fp = stream 95 | filename = getattr(fp, 'name', '') 96 | 97 | if mode == 'rb': 98 | gpfilebase = GPFileBase(fp, encoding) 99 | versionString = gpfilebase.readVersion() 100 | elif mode == 'wb': 101 | isClipboard = song.clipboard is not None 102 | if version is None: 103 | version = song.versionTuple 104 | if version is None: 105 | version = guessVersionByExtension(filename) 106 | versionString = _VERSIONS[(version, isClipboard)] 107 | 108 | version, GPFile = getVersionAndGPFile(versionString) 109 | gpfile = GPFile(fp, encoding, version=versionString, versionTuple=version) 110 | return gpfile, shouldClose 111 | 112 | 113 | def getVersionAndGPFile(versionString): 114 | try: 115 | return _GPFILES[versionString] 116 | except KeyError: 117 | raise GPException(f"unsupported version '{versionString}'") 118 | 119 | 120 | def guessVersionByExtension(filename): 121 | __, ext = os.path.splitext(filename) 122 | ext = ext.lstrip('.') 123 | version = _EXT_VERSIONS.get(ext) 124 | if version is None: 125 | version = _EXT_VERSIONS['gp5'] 126 | return version 127 | -------------------------------------------------------------------------------- /guitarpro/iobase.py: -------------------------------------------------------------------------------- 1 | import struct 2 | import logging 3 | from contextlib import contextmanager 4 | 5 | import attr 6 | 7 | from . import models as gp 8 | 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | @attr.s 14 | class GPFileBase: 15 | data = attr.ib() 16 | encoding = attr.ib() 17 | version = attr.ib(default=None) 18 | versionTuple = attr.ib(default=None) 19 | 20 | bendPosition = 60 21 | bendSemitone = 25 22 | 23 | _supportedVersions = [] 24 | 25 | _currentTrack = None 26 | _currentMeasureNumber = None 27 | _currentVoiceNumber = None 28 | _currentBeatNumber = None 29 | 30 | def close(self): 31 | self.data.close() 32 | 33 | def __enter__(self): 34 | return self 35 | 36 | def __exit__(self, *exc_info): 37 | self.close() 38 | 39 | # Reading 40 | # ======= 41 | 42 | def skip(self, count): 43 | return self.data.read(count) 44 | 45 | def read(self, fmt, count, default=None): 46 | try: 47 | data = self.data.read(count) 48 | result = struct.unpack(fmt, data) 49 | return result[0] 50 | except struct.error: 51 | if default is not None: 52 | return default 53 | else: 54 | raise 55 | 56 | def readByte(self, count=1, default=None): 57 | """Read 1 byte *count* times.""" 58 | args = ('B', 1) 59 | return (self.read(*args, default=default) if count == 1 else 60 | [self.read(*args, default=default) for i in range(count)]) 61 | 62 | def readSignedByte(self, count=1, default=None): 63 | """Read 1 signed byte *count* times.""" 64 | args = ('b', 1) 65 | return (self.read(*args, default=default) if count == 1 else 66 | [self.read(*args, default=default) for i in range(count)]) 67 | 68 | def readBool(self, count=1, default=None): 69 | """Read 1 byte *count* times as a boolean.""" 70 | args = ('?', 1) 71 | return (self.read(*args, default=default) if count == 1 else 72 | [self.read(*args, default=default) for i in range(count)]) 73 | 74 | def readShort(self, count=1, default=None): 75 | """Read 2 little-endian bytes *count* times as a short integer.""" 76 | args = (' 0 else length 102 | s = self.data.read(count) 103 | ss = s[:(length if length >= 0 else size)] 104 | return ss.decode(self.encoding) 105 | 106 | def readByteSizeString(self, size): 107 | """Read length of the string stored in 1 byte and followed by character 108 | bytes. 109 | """ 110 | return self.readString(size, self.readByte()) 111 | 112 | def readIntSizeString(self): 113 | """Read length of the string stored in 1 integer and followed by 114 | character bytes. 115 | """ 116 | return self.readString(self.readInt()) 117 | 118 | def readIntByteSizeString(self): 119 | """Read length of the string increased by 1 and stored in 1 integer 120 | followed by length of the string in 1 byte and finally followed by 121 | character bytes. 122 | """ 123 | d = self.readInt() - 1 124 | return self.readByteSizeString(d) 125 | 126 | def readVersion(self): 127 | if self.version is None: 128 | self.version = self.readByteSizeString(30) 129 | return self.version 130 | 131 | @contextmanager 132 | def annotateErrors(self, action): 133 | self._currentTrack = None 134 | self._currentMeasureNumber = None 135 | self._currentVoiceNumber = None 136 | self._currentBeatNumber = None 137 | try: 138 | yield 139 | except Exception as err: 140 | location = self.getCurrentLocation() 141 | if not location: 142 | raise 143 | raise gp.GPException(f"{action} {', '.join(location)}, " 144 | f"got {err.__class__.__name__}: {err}") from err 145 | finally: 146 | self._currentTrack = None 147 | self._currentMeasureNumber = None 148 | self._currentVoiceNumber = None 149 | self._currentBeatNumber = None 150 | 151 | def getEnumValue(self, enum): 152 | if enum.name == 'unknown': 153 | location = self.getCurrentLocation() 154 | logger.warning(f"{enum.value!r} is an unknown {enum.__class__.__name__} in {', '.join(location)}") 155 | return enum.value 156 | 157 | def getCurrentLocation(self): 158 | location = [] 159 | if self._currentTrack is not None: 160 | location.append(f"track {self._currentTrack.number}") 161 | if self._currentMeasureNumber is not None: 162 | location.append(f"measure {self._currentMeasureNumber}") 163 | if self._currentVoiceNumber is not None: 164 | location.append(f"voice {self._currentVoiceNumber}") 165 | if self._currentBeatNumber is not None: 166 | location.append(f"beat {self._currentBeatNumber}") 167 | return location 168 | 169 | # Writing 170 | # ======= 171 | 172 | def placeholder(self, count, byte=b'\x00'): 173 | self.data.write(byte * count) 174 | 175 | def writeByte(self, data): 176 | packed = struct.pack('B', int(data)) 177 | self.data.write(packed) 178 | 179 | def writeSignedByte(self, data): 180 | packed = struct.pack('b', int(data)) 181 | self.data.write(packed) 182 | 183 | def writeBool(self, data): 184 | packed = struct.pack('?', bool(data)) 185 | self.data.write(packed) 186 | 187 | def writeShort(self, data): 188 | packed = struct.pack(' Callable[[_T], _T]: 65 | return lambda a: a 66 | 67 | 68 | @overload 69 | @__dataclass_transform__(order_default=True, field_descriptors=(attr.attrib, attr.field)) 70 | def hashableAttrs(cls: _C, *, repr: bool = ...) -> _C: ... 71 | @overload 72 | @__dataclass_transform__(order_default=True, field_descriptors=(attr.attrib, attr.field)) 73 | def hashableAttrs(cls: None = ..., *, repr: bool = ...) -> Callable[[_C], _C]: ... 74 | def hashableAttrs(cls=None, *, repr=True): # noqa: E302 75 | """A fully hashable attrs decorator. 76 | 77 | Converts unhashable attributes, e.g. lists, to hashable ones, e.g. 78 | tuples. 79 | """ 80 | if cls is None: 81 | return partial(hashableAttrs, repr=repr) 82 | 83 | decorated = attr.s(cls, hash=True, repr=repr, auto_attribs=True) 84 | origHash = decorated.__hash__ 85 | 86 | def hash_(self): 87 | toEvolve = {} 88 | for field in attr.fields(self.__class__): 89 | value = getattr(self, field.name) 90 | if isinstance(value, (list, set)): 91 | new_value = tuple(value) 92 | toEvolve[field.name] = new_value 93 | newSelf = attr.evolve(self, **toEvolve) 94 | return origHash(newSelf) 95 | 96 | decorated.__hash__ = hash_ 97 | return decorated 98 | 99 | 100 | @hashableAttrs 101 | class RepeatGroup: 102 | """This class can store the information about a group of measures 103 | which are repeated. 104 | """ 105 | 106 | measureHeaders: List['MeasureHeader'] = attr.Factory(list) 107 | closings: List['MeasureHeader'] = attr.Factory(list) 108 | openings: List['MeasureHeader'] = attr.Factory(list) 109 | isClosed: bool = False 110 | 111 | def addMeasureHeader(self, h): 112 | if not len(self.openings): 113 | self.openings.append(h) 114 | 115 | self.measureHeaders.append(h) 116 | h.repeatGroup = self 117 | 118 | if h.repeatClose > 0: 119 | self.closings.append(h) 120 | self.isClosed = True 121 | # A new item after the header was closed? -> repeat alternative 122 | # reopens the group 123 | elif self.isClosed: 124 | self.isClosed = False 125 | self.openings.append(h) 126 | 127 | 128 | @hashableAttrs 129 | class Clipboard: 130 | startMeasure: int = 1 131 | stopMeasure: int = 1 132 | startTrack: int = 1 133 | stopTrack: int = 1 134 | startBeat: int = 1 135 | stopBeat: int = 1 136 | subBarCopy: bool = False 137 | 138 | 139 | class KeySignature(Enum): 140 | FMajorFlat = (-8, 0) 141 | CMajorFlat = (-7, 0) 142 | GMajorFlat = (-6, 0) 143 | DMajorFlat = (-5, 0) 144 | AMajorFlat = (-4, 0) 145 | EMajorFlat = (-3, 0) 146 | BMajorFlat = (-2, 0) 147 | FMajor = (-1, 0) 148 | CMajor = (0, 0) 149 | GMajor = (1, 0) 150 | DMajor = (2, 0) 151 | AMajor = (3, 0) 152 | EMajor = (4, 0) 153 | BMajor = (5, 0) 154 | FMajorSharp = (6, 0) 155 | CMajorSharp = (7, 0) 156 | GMajorSharp = (8, 0) 157 | 158 | DMinorFlat = (-8, 1) 159 | AMinorFlat = (-7, 1) 160 | EMinorFlat = (-6, 1) 161 | BMinorFlat = (-5, 1) 162 | FMinor = (-4, 1) 163 | CMinor = (-3, 1) 164 | GMinor = (-2, 1) 165 | DMinor = (-1, 1) 166 | AMinor = (0, 1) 167 | EMinor = (1, 1) 168 | BMinor = (2, 1) 169 | FMinorSharp = (3, 1) 170 | CMinorSharp = (4, 1) 171 | GMinorSharp = (5, 1) 172 | DMinorSharp = (6, 1) 173 | AMinorSharp = (7, 1) 174 | EMinorSharp = (8, 1) 175 | 176 | 177 | @hashableAttrs 178 | class LyricLine: 179 | """A lyrics line.""" 180 | 181 | startingMeasure: int = 1 182 | lyrics: str = '' 183 | 184 | 185 | @hashableAttrs(repr=False) 186 | class Lyrics: 187 | """A collection of lyrics lines for a track.""" 188 | 189 | trackChoice: int = 0 190 | lines: List[LyricLine] = attr.Factory(lambda: [LyricLine() for _ in range(Lyrics.maxLineCount)]) 191 | 192 | maxLineCount = 5 193 | 194 | def __str__(self): 195 | full = '' 196 | for line in self.lines: 197 | if line is not None: 198 | full += line.lyrics + '\n' 199 | ret = full.strip() 200 | ret = ret.replace('\n', ' ') 201 | ret = ret.replace('\r', ' ') 202 | return ret 203 | 204 | 205 | @hashableAttrs 206 | class Point: 207 | """A point construct using integer coordinates.""" 208 | 209 | x: int 210 | y: int 211 | 212 | 213 | @hashableAttrs 214 | class Padding: 215 | """A padding construct.""" 216 | 217 | right: int 218 | top: int 219 | left: int 220 | bottom: int 221 | 222 | 223 | class HeaderFooterElements(IntEnum): 224 | """An enumeration of the elements which can be shown in the header 225 | and footer of a rendered song sheet. 226 | 227 | All values can be combined using bit-operators as they are flags. 228 | """ 229 | 230 | none = 0x000 231 | title = 0x001 232 | subtitle = 0x002 233 | artist = 0x004 234 | album = 0x008 235 | words = 0x010 236 | music = 0x020 237 | wordsAndMusic = 0x040 238 | copyright = 0x080 239 | pageNumber = 0x100 240 | all = title | subtitle | artist | album | words | music | wordsAndMusic | copyright | pageNumber 241 | 242 | 243 | @hashableAttrs 244 | class PageSetup: 245 | """The page setup describes how the document is rendered. 246 | 247 | Page setup contains page size, margins, paddings, and how the title 248 | elements are rendered. 249 | 250 | Following template vars are available for defining the page texts: 251 | 252 | - ``%title%``: will be replaced with Song.title 253 | - ``%subtitle%``: will be replaced with Song.subtitle 254 | - ``%artist%``: will be replaced with Song.artist 255 | - ``%album%``: will be replaced with Song.album 256 | - ``%words%``: will be replaced with Song.words 257 | - ``%music%``: will be replaced with Song.music 258 | - ``%WORDSANDMUSIC%``: will be replaced with the according word 259 | and music values 260 | - ``%copyright%``: will be replaced with Song.copyright 261 | - ``%N%``: will be replaced with the current page number (if 262 | supported by layout) 263 | - ``%P%``: will be replaced with the number of pages (if supported 264 | by layout) 265 | """ 266 | 267 | pageSize: Point = Point(210, 297) 268 | pageMargin: Padding = Padding(10, 15, 10, 10) 269 | scoreSizeProportion: float = 1.0 270 | headerAndFooter: HeaderFooterElements = HeaderFooterElements.all 271 | title: str = '%title%' 272 | subtitle: str = '%subtitle%' 273 | artist: str = '%artist%' 274 | album: str = '%album%' 275 | words: str = 'Words by %words%' 276 | music: str = 'Music by %music%' 277 | wordsAndMusic: str = 'Words & Music by %WORDSMUSIC%' 278 | copyright: str = 'Copyright %copyright%\nAll Rights Reserved - International Copyright Secured' 279 | pageNumber: str = 'Page %N%/%P%' 280 | 281 | 282 | @hashableAttrs 283 | class RSEEqualizer: 284 | """Equalizer found in master effect and track effect. 285 | 286 | Attribute :attr:`RSEEqualizer.knobs` is a list of values in range 287 | from -6.0 to 5.9. Master effect has 10 knobs, track effect has 3 288 | knobs. Gain is a value in range from -6.0 to 5.9 which can be found 289 | in both master and track effects and is named as "PRE" in Guitar Pro 290 | 5. 291 | """ 292 | 293 | knobs: List[float] = attr.Factory(list) 294 | gain: float = attr.ib(default=0.0) 295 | 296 | 297 | @hashableAttrs 298 | class RSEMasterEffect: 299 | """Master effect as seen in "Score information".""" 300 | 301 | volume: float = 0 302 | reverb: float = 0 303 | equalizer: RSEEqualizer = attr.Factory(RSEEqualizer) 304 | 305 | def __attrs_post_init__(self): 306 | if not self.equalizer.knobs: 307 | self.equalizer.knobs = [0.0] * 10 308 | 309 | 310 | @hashableAttrs(repr=False) 311 | class Song: 312 | """The top-level node of the song model. 313 | 314 | It contains basic information about the stored song. 315 | """ 316 | 317 | # TODO: Store file format version here 318 | versionTuple: Optional[Tuple[int, int, int]] = attr.ib(default=None, hash=False, eq=False) 319 | clipboard: Optional[Clipboard] = None 320 | title: str = '' 321 | subtitle: str = '' 322 | artist: str = '' 323 | album: str = '' 324 | words: str = '' 325 | music: str = '' 326 | copyright: str = '' 327 | tab: str = '' 328 | instructions: str = '' 329 | notice: List[str] = attr.Factory(list) 330 | lyrics: Lyrics = attr.Factory(Lyrics) 331 | pageSetup: PageSetup = attr.Factory(PageSetup) 332 | tempoName: str = 'Moderate' 333 | tempo: int = 120 334 | hideTempo: bool = False 335 | key: KeySignature = KeySignature.CMajor 336 | measureHeaders: List['MeasureHeader'] = attr.Factory(lambda: [MeasureHeader()]) 337 | tracks: List['Track'] = attr.Factory(lambda self: [Track(self)], takes_self=True) 338 | masterEffect: RSEMasterEffect = attr.Factory(RSEMasterEffect) 339 | 340 | _currentRepeatGroup: RepeatGroup = attr.ib(default=attr.Factory(RepeatGroup), hash=False, eq=False, repr=False) 341 | 342 | def addMeasureHeader(self, header): 343 | header.song = self 344 | self.measureHeaders.append(header) 345 | 346 | # if the group is closed only the next upcoming header can 347 | # reopen the group in case of a repeat alternative, so we remove 348 | # the current group 349 | if header.isRepeatOpen or self._currentRepeatGroup.isClosed and header.repeatAlternative <= 0: 350 | self._currentRepeatGroup = RepeatGroup() 351 | 352 | self._currentRepeatGroup.addMeasureHeader(header) 353 | 354 | def newMeasure(self): 355 | header = MeasureHeader() 356 | self.measureHeaders.append(header) 357 | for track in self.tracks: 358 | measure = Measure(track, header) 359 | track.measures.append(measure) 360 | 361 | 362 | @hashableAttrs 363 | class MidiChannel: 364 | """A MIDI channel describes playing data for a track.""" 365 | 366 | channel: int = 0 367 | effectChannel: int = 1 368 | instrument: int = 25 369 | volume: int = 104 370 | balance: int = 64 371 | chorus: int = 0 372 | reverb: int = 0 373 | phaser: int = 0 374 | tremolo: int = 0 375 | bank: int = 0 376 | 377 | DEFAULT_PERCUSSION_CHANNEL = 9 378 | 379 | @property 380 | def isPercussionChannel(self): 381 | return self.channel % 16 == self.DEFAULT_PERCUSSION_CHANNEL 382 | 383 | 384 | @hashableAttrs 385 | class DirectionSign: 386 | """A navigation sign like *Coda* or *Segno*.""" 387 | 388 | # TODO: Consider making DirectionSign an Enum. 389 | name: str = '' 390 | 391 | 392 | @hashableAttrs 393 | class Tuplet: 394 | """A *n:m* tuplet.""" 395 | 396 | enters: int = 1 397 | times: int = 1 398 | 399 | supportedTuplets = [ 400 | (1, 1), 401 | (3, 2), 402 | (5, 4), 403 | (6, 4), 404 | (7, 4), 405 | (9, 8), 406 | (10, 8), 407 | (11, 8), 408 | (12, 8), 409 | (13, 8), 410 | ] 411 | 412 | def convertTime(self, time): 413 | result = Fraction(time * self.times, self.enters) 414 | if result.denominator == 1: 415 | return result.numerator 416 | return result 417 | 418 | def isSupported(self): 419 | return (self.enters, self.times) in self.supportedTuplets 420 | 421 | @classmethod 422 | def fromFraction(cls, frac): 423 | return cls(frac.denominator, frac.numerator) 424 | 425 | 426 | @hashableAttrs 427 | class Duration: 428 | """A duration.""" 429 | 430 | quarterTime = 960 431 | 432 | whole = 1 433 | half = 2 434 | quarter = 4 435 | eighth = 8 436 | sixteenth = 16 437 | thirtySecond = 32 438 | sixtyFourth = 64 439 | hundredTwentyEighth = 128 440 | 441 | # The time resulting with a 64th note and a 3/2 tuplet 442 | minTime = quarterTime * 4 // sixtyFourth * 2 // 3 443 | 444 | value: int = quarter 445 | isDotted: bool = False 446 | tuplet: Tuplet = attr.Factory(Tuplet) 447 | 448 | @property 449 | def time(self): 450 | result = self.quarterTime * 4 // self.value 451 | if self.isDotted: 452 | result += result // 2 453 | return self.tuplet.convertTime(result) 454 | 455 | @property 456 | def index(self): 457 | index = 0 458 | value = self.value 459 | while True: 460 | value = (value >> 1) 461 | if value > 0: 462 | index += 1 463 | else: 464 | break 465 | return index 466 | 467 | @classmethod 468 | def fromTime(cls, time): 469 | timeFrac = Fraction(time, cls.quarterTime * 4) 470 | exp = int(log(timeFrac, 2)) 471 | value = 2 ** -exp 472 | tuplet = Tuplet.fromFraction(timeFrac * value) 473 | isDotted = False 474 | if not tuplet.isSupported(): 475 | # Check if it's dotted 476 | timeFrac = Fraction(time, cls.quarterTime * 4) * Fraction(2, 3) 477 | exp = int(log(timeFrac, 2)) 478 | value = 2 ** -exp 479 | tuplet = Tuplet.fromFraction(timeFrac * value) 480 | isDotted = True 481 | if not tuplet.isSupported(): 482 | raise ValueError(f'cannot represent time {time} as a Guitar Pro duration') 483 | return Duration(value, isDotted, tuplet) 484 | 485 | 486 | @hashableAttrs 487 | class TimeSignature: 488 | """A time signature.""" 489 | 490 | numerator: int = 4 491 | denominator: Duration = attr.Factory(Duration) 492 | beams: List[int] = attr.Factory(list) 493 | 494 | def __attrs_post_init__(self): 495 | if not self.beams: 496 | self.beams = [2, 2, 2, 2] 497 | 498 | 499 | class TripletFeel(Enum): 500 | """An enumeration of different triplet feels.""" 501 | 502 | #: No triplet feel. 503 | none = 0 504 | 505 | #: Eighth triplet feel. 506 | eighth = 1 507 | 508 | #: Sixteenth triplet feel. 509 | sixteenth = 2 510 | 511 | 512 | @hashableAttrs(repr=False) 513 | class MeasureHeader: 514 | """A measure header contains metadata for measures over multiple 515 | tracks. 516 | """ 517 | 518 | number: int = attr.ib(default=1, hash=False, eq=False) 519 | start: int = attr.ib(default=Duration.quarterTime, hash=False, eq=False) 520 | hasDoubleBar: bool = False 521 | keySignature: KeySignature = KeySignature.CMajor 522 | timeSignature: TimeSignature = attr.Factory(TimeSignature) 523 | marker: Optional['Marker'] = None 524 | isRepeatOpen: bool = False 525 | repeatAlternative: int = 0 526 | repeatClose: int = -1 527 | tripletFeel: TripletFeel = TripletFeel.none 528 | direction: Optional[DirectionSign] = None 529 | fromDirection: Optional[DirectionSign] = None 530 | 531 | @property 532 | def length(self): 533 | return self.timeSignature.numerator * self.timeSignature.denominator.time 534 | 535 | @property 536 | def end(self): 537 | return self.start + self.length 538 | 539 | 540 | @hashableAttrs 541 | class Color: 542 | """An RGB Color.""" 543 | 544 | r: int 545 | g: int 546 | b: int 547 | 548 | 549 | Color.black = Color(0, 0, 0) 550 | Color.red = Color(255, 0, 0) 551 | 552 | 553 | @hashableAttrs 554 | class Marker: 555 | """A marker annotation for beats.""" 556 | 557 | title: str = 'Section' 558 | color: Color = Color.red 559 | 560 | 561 | @hashableAttrs 562 | class TrackSettings: 563 | """Settings of the track.""" 564 | 565 | tablature: bool = True 566 | notation: bool = True 567 | diagramsAreBelow: bool = False 568 | showRhythm: bool = False 569 | forceHorizontal: bool = False 570 | forceChannels: bool = False 571 | diagramList: bool = True 572 | diagramsInScore: bool = False 573 | autoLetRing: bool = False 574 | autoBrush: bool = False 575 | extendRhythmic: bool = False 576 | 577 | 578 | class Accentuation(Enum): 579 | """Values of auto-accentuation on the beat found in track RSE 580 | settings. 581 | """ 582 | 583 | #: No auto-accentuation. 584 | none = 0 585 | 586 | #: Very soft accentuation. 587 | verySoft = 1 588 | 589 | #: Soft accentuation. 590 | soft = 2 591 | 592 | #: Medium accentuation. 593 | medium = 3 594 | 595 | #: Strong accentuation. 596 | strong = 4 597 | 598 | #: Very strong accentuation. 599 | veryStrong = 5 600 | 601 | 602 | @hashableAttrs 603 | class RSEInstrument: 604 | instrument: int = -1 605 | unknown: int = -1 606 | soundBank: int = -1 607 | effectNumber: int = -1 608 | effectCategory: str = '' 609 | effect: str = '' 610 | 611 | 612 | @hashableAttrs(repr=False) 613 | class TrackRSE: 614 | instrument: RSEInstrument = attr.Factory(RSEInstrument) 615 | equalizer: RSEEqualizer = attr.Factory(RSEEqualizer) 616 | humanize: int = 0 617 | autoAccentuation: Accentuation = Accentuation.none 618 | 619 | def __attrs_post_init__(self): 620 | if not self.equalizer.knobs: 621 | self.equalizer.knobs = [0.0] * 3 622 | 623 | 624 | @hashableAttrs(repr=False) 625 | class Track: 626 | """A track contains multiple measures.""" 627 | 628 | song: Song = attr.ib(hash=False, eq=False, repr=False) 629 | number: int = attr.ib(default=1, hash=False, eq=False) 630 | fretCount: int = 24 631 | offset: int = 0 632 | isPercussionTrack: bool = False 633 | is12StringedGuitarTrack: bool = False 634 | isBanjoTrack: bool = False 635 | isVisible: bool = True 636 | isSolo: bool = False 637 | isMute: bool = False 638 | indicateTuning: bool = False 639 | name: str = 'Track 1' 640 | measures: List['Measure'] = attr.Factory(lambda self: [Measure(self, header) 641 | for header in self.song.measureHeaders], 642 | takes_self=True) 643 | strings: List['GuitarString'] = attr.Factory(lambda: [GuitarString(n, v) 644 | for n, v in [(1, 64), (2, 59), (3, 55), 645 | (4, 50), (5, 45), (6, 40)]]) 646 | port: int = 1 647 | channel: MidiChannel = attr.Factory(MidiChannel) 648 | color: Color = Color.red 649 | settings: TrackSettings = attr.Factory(TrackSettings) 650 | useRSE: bool = False 651 | rse: TrackRSE = attr.Factory(TrackRSE) 652 | 653 | 654 | @hashableAttrs 655 | class GuitarString: 656 | """A guitar string with a special tuning.""" 657 | 658 | number: int 659 | value: int 660 | 661 | def __str__(self): 662 | notes = 'C C# D D# E F F# G G# A A# B'.split() 663 | octave, semitone = divmod(self.value, 12) 664 | return f'{notes[semitone]}{octave-1}' 665 | 666 | 667 | class MeasureClef(Enum): 668 | """An enumeration of available clefs.""" 669 | 670 | treble = 0 671 | bass = 1 672 | tenor = 2 673 | alto = 3 674 | 675 | 676 | class LineBreak(Enum): 677 | """A line break directive.""" 678 | 679 | #: No line break. 680 | none = 0 681 | #: Break line. 682 | break_ = 1 683 | #: Protect the line from breaking. 684 | protect = 2 685 | 686 | 687 | @hashableAttrs(repr=False) 688 | class Measure: 689 | """A measure contains multiple voices of beats.""" 690 | 691 | track: Track = attr.ib(hash=False, eq=False, repr=False) 692 | header: MeasureHeader = attr.ib(hash=False, eq=False, repr=False) 693 | clef: MeasureClef = MeasureClef.treble 694 | voices: List['Voice'] = attr.Factory(lambda self: [Voice(self) for _ in range(self.maxVoices)], takes_self=True) 695 | lineBreak: LineBreak = LineBreak.none 696 | 697 | maxVoices = 2 698 | 699 | @property 700 | def isEmpty(self): 701 | return all(voice.isEmpty for voice in self.voices) 702 | 703 | def _promote_header_attr(name): 704 | def fget(self): 705 | return getattr(self.header, name) 706 | 707 | def fset(self, value): 708 | setattr(self.header, name, value) 709 | 710 | return property(fget, fset) 711 | 712 | number = _promote_header_attr('number') 713 | keySignature = _promote_header_attr('keySignature') 714 | repeatClose = _promote_header_attr('repeatClose') 715 | start = _promote_header_attr('start') 716 | end = _promote_header_attr('end') 717 | length = _promote_header_attr('length') 718 | timeSignature = _promote_header_attr('timeSignature') 719 | isRepeatOpen = _promote_header_attr('isRepeatOpen') 720 | tripletFeel = _promote_header_attr('tripletFeel') 721 | marker = _promote_header_attr('marker') 722 | 723 | del _promote_header_attr 724 | 725 | 726 | class VoiceDirection(Enum): 727 | """Voice directions indicating the direction of beams.""" 728 | 729 | none = 0 730 | up = 1 731 | down = 2 732 | 733 | 734 | @hashableAttrs(repr=False) 735 | class Voice: 736 | """A voice contains multiple beats.""" 737 | 738 | measure: Measure = attr.ib(hash=False, eq=False, repr=False) 739 | beats: List['Beat'] = attr.Factory(list) 740 | direction: VoiceDirection = VoiceDirection.none 741 | 742 | @property 743 | def isEmpty(self): 744 | return len(self.beats) == 0 745 | 746 | 747 | class BeatStrokeDirection(Enum): 748 | """All beat stroke directions.""" 749 | 750 | none = 0 751 | up = 1 752 | down = 2 753 | 754 | 755 | @hashableAttrs 756 | class BeatStroke: 757 | """A stroke effect for beats.""" 758 | 759 | direction: BeatStrokeDirection = BeatStrokeDirection.none 760 | value: int = 0 761 | 762 | def swapDirection(self): 763 | if self.direction == BeatStrokeDirection.up: 764 | return attr.evolve(self, direction=BeatStrokeDirection.down) 765 | elif self.direction == BeatStrokeDirection.down: 766 | return attr.evolve(self, direction=BeatStrokeDirection.up) 767 | return self 768 | 769 | 770 | class SlapEffect(Enum): 771 | """Characteristic of articulation.""" 772 | 773 | #: No slap effect. 774 | none = 0 775 | 776 | #: Tapping. 777 | tapping = 1 778 | 779 | #: Slapping. 780 | slapping = 2 781 | 782 | #: Popping. 783 | popping = 3 784 | 785 | 786 | @hashableAttrs 787 | class BeatEffect: 788 | """This class contains all beat effects.""" 789 | 790 | stroke: BeatStroke = attr.Factory(BeatStroke) 791 | hasRasgueado: bool = False 792 | pickStroke: BeatStrokeDirection = BeatStrokeDirection.none 793 | chord: Optional['Chord'] = None 794 | fadeIn: bool = False 795 | tremoloBar: Optional['BendEffect'] = None 796 | mixTableChange: Optional['MixTableChange'] = None 797 | slapEffect: SlapEffect = SlapEffect.none 798 | vibrato: bool = False 799 | 800 | @property 801 | def isChord(self): 802 | return self.chord is not None 803 | 804 | @property 805 | def isTremoloBar(self): 806 | return self.tremoloBar is not None 807 | 808 | @property 809 | def isSlapEffect(self): 810 | return self.slapEffect != SlapEffect.none 811 | 812 | @property 813 | def hasPickStroke(self): 814 | return self.pickStroke != BeatStrokeDirection.none 815 | 816 | @property 817 | def isDefault(self): 818 | default = BeatEffect() 819 | return (self.stroke == default.stroke and 820 | self.hasRasgueado == default.hasRasgueado and 821 | self.pickStroke == default.pickStroke and 822 | self.fadeIn == default.fadeIn and 823 | self.vibrato == default.vibrato and 824 | self.tremoloBar == default.tremoloBar and 825 | self.slapEffect == default.slapEffect) 826 | 827 | 828 | class TupletBracket(Enum): 829 | none = 0 830 | start = 1 831 | end = 2 832 | 833 | 834 | @hashableAttrs 835 | class BeatDisplay: 836 | """Parameters of beat display.""" 837 | 838 | breakBeam: bool = False 839 | forceBeam: bool = False 840 | beamDirection: VoiceDirection = VoiceDirection.none 841 | tupletBracket: TupletBracket = TupletBracket.none 842 | breakSecondary: int = 0 843 | breakSecondaryTuplet: bool = False 844 | forceBracket: bool = False 845 | 846 | 847 | class Octave(Enum): 848 | """Octave signs.""" 849 | 850 | none = 0 851 | ottava = 1 852 | quindicesima = 2 853 | ottavaBassa = 3 854 | quindicesimaBassa = 4 855 | 856 | 857 | class BeatStatus(Enum): 858 | empty = 0 859 | normal = 1 860 | rest = 2 861 | 862 | 863 | @hashableAttrs(repr=False) 864 | class Beat: 865 | """A beat contains multiple notes.""" 866 | 867 | voice: Voice = attr.ib(hash=False, eq=False, repr=False) 868 | notes: List['Note'] = attr.Factory(list) 869 | duration: Duration = attr.Factory(Duration) 870 | text: Optional[str] = None 871 | start: Optional[int] = attr.ib(default=None, hash=False, eq=False) 872 | effect: BeatEffect = attr.Factory(BeatEffect) 873 | octave: Octave = Octave.none 874 | display: BeatDisplay = attr.Factory(BeatDisplay) 875 | status: BeatStatus = BeatStatus.empty 876 | 877 | @property 878 | def startInMeasure(self): 879 | offset = self.start - self.voice.measure.start 880 | return offset 881 | 882 | @property 883 | def hasVibrato(self): 884 | for note in self.notes: 885 | if note.effect.vibrato: 886 | return True 887 | return False 888 | 889 | @property 890 | def hasHarmonic(self): 891 | for note in self.notes: 892 | if note.effect.isHarmonic: 893 | return note.effect.harmonic 894 | 895 | 896 | @hashableAttrs 897 | class HarmonicEffect: 898 | """A harmonic note effect.""" 899 | 900 | type: int = attr.ib(init=False) 901 | 902 | 903 | @hashableAttrs 904 | class NaturalHarmonic(HarmonicEffect): 905 | def __attrs_post_init__(self): 906 | self.type = 1 907 | 908 | 909 | @hashableAttrs 910 | class ArtificialHarmonic(HarmonicEffect): 911 | pitch: Optional['PitchClass'] = None 912 | octave: Optional[int] = None 913 | 914 | def __attrs_post_init__(self): 915 | self.type = 2 916 | 917 | 918 | @hashableAttrs 919 | class TappedHarmonic(HarmonicEffect): 920 | fret: Optional[int] = None 921 | 922 | def __attrs_post_init__(self): 923 | self.type = 3 924 | 925 | 926 | @hashableAttrs 927 | class PinchHarmonic(HarmonicEffect): 928 | def __attrs_post_init__(self): 929 | self.type = 4 930 | 931 | 932 | @hashableAttrs 933 | class SemiHarmonic(HarmonicEffect): 934 | def __attrs_post_init__(self): 935 | self.type = 5 936 | 937 | 938 | class GraceEffectTransition(Enum): 939 | """All transition types for grace notes.""" 940 | 941 | #: No transition. 942 | none = 0 943 | 944 | #: Slide from the grace note to the real one. 945 | slide = 1 946 | 947 | #: Perform a bend from the grace note to the real one. 948 | bend = 2 949 | 950 | #: Perform a hammer on. 951 | hammer = 3 952 | 953 | 954 | class Velocities: 955 | """A collection of velocities / dynamics.""" 956 | 957 | minVelocity = 15 958 | velocityIncrement = 16 959 | pianoPianissimo = minVelocity 960 | pianissimo = minVelocity + velocityIncrement 961 | piano = minVelocity + velocityIncrement * 2 962 | mezzoPiano = minVelocity + velocityIncrement * 3 963 | mezzoForte = minVelocity + velocityIncrement * 4 964 | forte = minVelocity + velocityIncrement * 5 965 | fortissimo = minVelocity + velocityIncrement * 6 966 | forteFortissimo = minVelocity + velocityIncrement * 7 967 | default = forte 968 | 969 | 970 | @hashableAttrs 971 | class GraceEffect: 972 | """A grace note effect.""" 973 | 974 | duration: int = 32 975 | fret: int = 0 976 | isDead: bool = False 977 | isOnBeat: bool = False 978 | transition: GraceEffectTransition = GraceEffectTransition.none 979 | velocity: int = Velocities.default 980 | 981 | @property 982 | def durationTime(self): 983 | """Get the duration of the effect.""" 984 | return Duration.quarterTime * 4 // self.duration 985 | 986 | 987 | @hashableAttrs 988 | class TrillEffect: 989 | """A trill effect.""" 990 | 991 | fret: int = 0 992 | duration: Duration = attr.Factory(Duration) 993 | 994 | 995 | @hashableAttrs 996 | class TremoloPickingEffect: 997 | """A tremolo picking effect.""" 998 | 999 | duration: Duration = attr.Factory(Duration) 1000 | 1001 | 1002 | class SlideType(Enum): 1003 | """An enumeration of all supported slide types.""" 1004 | 1005 | intoFromAbove = -2 1006 | intoFromBelow = -1 1007 | none = 0 1008 | shiftSlideTo = 1 1009 | legatoSlideTo = 2 1010 | outDownwards = 3 1011 | outUpwards = 4 1012 | 1013 | 1014 | class Fingering(LenientEnum): 1015 | """Left and right hand fingering used in tabs and chord diagram 1016 | editor. 1017 | """ 1018 | 1019 | #: Open or muted. 1020 | open = -1 1021 | #: Thumb. 1022 | thumb = 0 1023 | #: Index finger. 1024 | index = 1 1025 | #: Middle finger. 1026 | middle = 2 1027 | #: Annular finger. 1028 | annular = 3 1029 | #: Little finger. 1030 | little = 4 1031 | 1032 | 1033 | @hashableAttrs(repr=False) 1034 | class NoteEffect: 1035 | """Contains all effects which can be applied to one note.""" 1036 | 1037 | accentuatedNote: bool = False 1038 | bend: Optional['BendEffect'] = None 1039 | ghostNote: bool = False 1040 | grace: Optional[GraceEffect] = None 1041 | hammer: bool = False 1042 | harmonic: Optional[HarmonicEffect] = None 1043 | heavyAccentuatedNote: bool = False 1044 | leftHandFinger: Fingering = Fingering.open 1045 | letRing: bool = False 1046 | palmMute: bool = False 1047 | rightHandFinger: Fingering = Fingering.open 1048 | slides: List[SlideType] = attr.Factory(list) 1049 | staccato: bool = False 1050 | tremoloPicking: Optional[TremoloPickingEffect] = None 1051 | trill: Optional[TrillEffect] = None 1052 | vibrato: bool = False 1053 | 1054 | @property 1055 | def isBend(self): 1056 | return self.bend is not None and len(self.bend.points) 1057 | 1058 | @property 1059 | def isHarmonic(self): 1060 | return self.harmonic is not None 1061 | 1062 | @property 1063 | def isGrace(self): 1064 | return self.grace is not None 1065 | 1066 | @property 1067 | def isTrill(self): 1068 | return self.trill is not None 1069 | 1070 | @property 1071 | def isTremoloPicking(self): 1072 | return self.tremoloPicking is not None 1073 | 1074 | @property 1075 | def isFingering(self): 1076 | return (self.leftHandFinger.value > -1 or 1077 | self.rightHandFinger.value > -1) 1078 | 1079 | @property 1080 | def isDefault(self): 1081 | default = NoteEffect() 1082 | return (self.leftHandFinger == default.leftHandFinger and 1083 | self.rightHandFinger == default.rightHandFinger and 1084 | self.bend == default.bend and 1085 | self.harmonic == default.harmonic and 1086 | self.grace == default.grace and 1087 | self.trill == default.trill and 1088 | self.tremoloPicking == default.tremoloPicking and 1089 | self.vibrato == default.vibrato and 1090 | self.slides == default.slides and 1091 | self.hammer == default.hammer and 1092 | self.palmMute == default.palmMute and 1093 | self.staccato == default.staccato and 1094 | self.letRing == default.letRing) 1095 | 1096 | 1097 | class NoteType(LenientEnum): 1098 | rest = 0 1099 | normal = 1 1100 | tie = 2 1101 | dead = 3 1102 | 1103 | 1104 | @hashableAttrs 1105 | class Note: 1106 | """Describes a single note.""" 1107 | 1108 | beat: Beat = attr.ib(hash=False, eq=False, repr=False) 1109 | value: int = 0 1110 | velocity: int = Velocities.default 1111 | string: int = 0 1112 | effect: NoteEffect = attr.Factory(NoteEffect) 1113 | durationPercent: float = 1.0 1114 | swapAccidentals: bool = False 1115 | type: NoteType = NoteType.rest 1116 | 1117 | @property 1118 | def realValue(self): 1119 | return self.value + self.beat.voice.measure.track.strings[self.string - 1].value 1120 | 1121 | 1122 | @hashableAttrs 1123 | class Chord: 1124 | """A chord annotation for beats.""" 1125 | 1126 | length: int 1127 | sharp: Optional[bool] = None 1128 | root: Optional['PitchClass'] = None 1129 | type: Optional['ChordType'] = None 1130 | extension: Optional['ChordExtension'] = None 1131 | bass: Optional['PitchClass'] = None 1132 | tonality: Optional['ChordAlteration'] = None 1133 | add: Optional[bool] = None 1134 | name: str = '' 1135 | fifth: Optional['ChordAlteration'] = None 1136 | ninth: Optional['ChordAlteration'] = None 1137 | eleventh: Optional['ChordAlteration'] = None 1138 | firstFret: Optional[int] = None 1139 | strings: List[int] = attr.Factory(lambda self: [-1] * self.length, takes_self=True) 1140 | barres: List['Barre'] = attr.Factory(list) 1141 | omissions: List[bool] = attr.Factory(list) 1142 | fingerings: List[Fingering] = attr.Factory(list) 1143 | show: Optional[bool] = None 1144 | newFormat: Optional[bool] = None 1145 | 1146 | @property 1147 | def notes(self): 1148 | return [string for string in self.strings if string >= 0] 1149 | 1150 | 1151 | class ChordType(LenientEnum): 1152 | """Type of the chord.""" 1153 | 1154 | #: Major chord. 1155 | major = 0 1156 | 1157 | #: Dominant seventh chord. 1158 | seventh = 1 1159 | 1160 | #: Major seventh chord. 1161 | majorSeventh = 2 1162 | 1163 | #: Add sixth chord. 1164 | sixth = 3 1165 | 1166 | #: Minor chord. 1167 | minor = 4 1168 | 1169 | #: Minor seventh chord. 1170 | minorSeventh = 5 1171 | 1172 | #: Minor major seventh chord. 1173 | minorMajor = 6 1174 | 1175 | #: Minor add sixth chord. 1176 | minorSixth = 7 1177 | 1178 | #: Suspended second chord. 1179 | suspendedSecond = 8 1180 | 1181 | #: Suspended fourth chord. 1182 | suspendedFourth = 9 1183 | 1184 | #: Seventh suspended second chord. 1185 | seventhSuspendedSecond = 10 1186 | 1187 | #: Seventh suspended fourth chord. 1188 | seventhSuspendedFourth = 11 1189 | 1190 | #: Diminished chord. 1191 | diminished = 12 1192 | 1193 | #: Augmented chord. 1194 | augmented = 13 1195 | 1196 | #: Power chord. 1197 | power = 14 1198 | 1199 | 1200 | @hashableAttrs 1201 | class Barre: 1202 | """A single barre. 1203 | 1204 | :param start: first string from the bottom of the barre. 1205 | :param end: last string on the top of the barre. 1206 | """ 1207 | 1208 | fret: int 1209 | start: int = 0 1210 | end: int = 0 1211 | 1212 | @property 1213 | def range(self): 1214 | return self.start, self.end 1215 | 1216 | 1217 | class ChordAlteration(Enum): 1218 | """Tonality of the chord.""" 1219 | 1220 | #: Perfect. 1221 | perfect = 0 1222 | 1223 | #: Diminished. 1224 | diminished = 1 1225 | 1226 | #: Augmented. 1227 | augmented = 2 1228 | 1229 | 1230 | class ChordExtension(LenientEnum): 1231 | """Extension type of the chord.""" 1232 | 1233 | #: No extension. 1234 | none = 0 1235 | 1236 | #: Ninth chord. 1237 | ninth = 1 1238 | 1239 | #: Eleventh chord. 1240 | eleventh = 2 1241 | 1242 | #: Thirteenth chord. 1243 | thirteenth = 3 1244 | 1245 | 1246 | @hashableAttrs 1247 | class PitchClass: 1248 | """A pitch class. 1249 | 1250 | Constructor provides several overloads. Each overload provides 1251 | keyword argument *intonation* that may be either "sharp" or "flat". 1252 | 1253 | First of overloads is (tone, accidental): 1254 | 1255 | :param tone: integer of whole-tone. 1256 | :param accidental: flat (-1), none (0) or sharp (1). 1257 | 1258 | >>> p = PitchClass(4, -1) 1259 | >>> p 1260 | PitchClass(just=4, accidental=-1, value=3, intonation='flat') 1261 | >>> print(p) 1262 | Eb 1263 | >>> p = PitchClass(4, -1, intonation='sharp') 1264 | >>> p 1265 | PitchClass(just=4, accidental=-1, value=3, intonation='sharp') 1266 | >>> print(p) 1267 | D# 1268 | 1269 | Second, semitone number can be directly passed to constructor: 1270 | 1271 | :param semitone: integer of semitone. 1272 | 1273 | >>> p = PitchClass(3) 1274 | >>> print(p) 1275 | Eb 1276 | >>> p = PitchClass(3, intonation='sharp') 1277 | >>> print(p) 1278 | D# 1279 | 1280 | And last, but not least, note name: 1281 | 1282 | :param name: string representing note. 1283 | 1284 | >>> p = PitchClass('D#') 1285 | >>> print(p) 1286 | D# 1287 | """ 1288 | 1289 | just: Union[str, int] 1290 | accidental: Optional[int] = None 1291 | value: Optional[int] = None 1292 | intonation: Optional[str] = None 1293 | 1294 | _notes = { 1295 | 'sharp': 'C C# D D# E F F# G G# A A# B'.split(), 1296 | 'flat': 'C Db D Eb E F Gb G Ab A Bb B'.split(), 1297 | } 1298 | 1299 | def __attrs_post_init__(self): 1300 | if self.accidental is None: 1301 | if isinstance(self.just, str): 1302 | # Assume string input 1303 | string = self.just 1304 | try: 1305 | value = self._notes['sharp'].index(string) 1306 | except ValueError: 1307 | value = self._notes['flat'].index(string) 1308 | elif isinstance(self.just, int): 1309 | value = self.just % 12 1310 | try: 1311 | string = self._notes['sharp'][value] 1312 | except KeyError: 1313 | string = self._notes['flat'][value] 1314 | if string.endswith('b'): 1315 | accidental = -1 1316 | elif string.endswith('#'): 1317 | accidental = 1 1318 | else: 1319 | accidental = 0 1320 | pitch = value - accidental 1321 | else: 1322 | pitch, accidental = self.just, self.accidental 1323 | 1324 | self.just = pitch % 12 1325 | self.accidental = accidental 1326 | self.value = self.just + accidental 1327 | if self.intonation is None: 1328 | if accidental == -1: 1329 | self.intonation = 'flat' 1330 | else: 1331 | self.intonation = 'sharp' 1332 | 1333 | def __str__(self): 1334 | return self._notes[self.intonation][self.value] 1335 | 1336 | 1337 | @hashableAttrs 1338 | class MixTableItem: 1339 | """A mix table item describes a mix parameter, e.g. volume or 1340 | reverb. 1341 | """ 1342 | 1343 | value: int = 0 1344 | duration: int = 0 1345 | allTracks: bool = False 1346 | 1347 | 1348 | @hashableAttrs 1349 | class WahEffect: 1350 | value: int = attr.ib(default=-1) 1351 | display: bool = False 1352 | 1353 | @value.validator 1354 | def checkValue(self, attrib, value): 1355 | if not -2 <= value <= 100: 1356 | raise ValueError('value must be in range from -2 to 100') 1357 | 1358 | def isOff(self): 1359 | return self.value == WahEffect.off.value 1360 | 1361 | def isNone(self): 1362 | return self.value == WahEffect.none.value 1363 | 1364 | def isOn(self): 1365 | return 0 <= self.value <= 100 1366 | 1367 | 1368 | WahEffect.off = WahEffect(-2) 1369 | WahEffect.none = WahEffect(-1) 1370 | 1371 | 1372 | @hashableAttrs 1373 | class MixTableChange: 1374 | """A MixTableChange describes a change in mix parameters.""" 1375 | 1376 | instrument: Optional[MixTableItem] = None 1377 | rse: RSEInstrument = attr.Factory(RSEInstrument) 1378 | volume: Optional[MixTableItem] = None 1379 | balance: Optional[MixTableItem] = None 1380 | chorus: Optional[MixTableItem] = None 1381 | reverb: Optional[MixTableItem] = None 1382 | phaser: Optional[MixTableItem] = None 1383 | tremolo: Optional[MixTableItem] = None 1384 | tempoName: str = '' 1385 | tempo: Optional[MixTableItem] = None 1386 | hideTempo: bool = True 1387 | wah: Optional[WahEffect] = None 1388 | useRSE: bool = False 1389 | 1390 | @property 1391 | def isJustWah(self): 1392 | return (self.instrument is None and 1393 | self.volume is None and 1394 | self.balance is None and 1395 | self.chorus is None and 1396 | self.reverb is None and 1397 | self.phaser is None and 1398 | self.tremolo is None and 1399 | self.tempo is None and 1400 | self.wah is not None) 1401 | 1402 | 1403 | class BendType(Enum): 1404 | """All Bend presets.""" 1405 | 1406 | #: No Preset. 1407 | none = 0 1408 | 1409 | # Bends 1410 | # ===== 1411 | 1412 | #: A simple bend. 1413 | bend = 1 1414 | 1415 | #: A bend and release afterwards. 1416 | bendRelease = 2 1417 | 1418 | #: A bend, then release and rebend. 1419 | bendReleaseBend = 3 1420 | 1421 | #: Prebend. 1422 | prebend = 4 1423 | 1424 | #: Prebend and then release. 1425 | prebendRelease = 5 1426 | 1427 | # Tremolo Bar 1428 | # =========== 1429 | 1430 | #: Dip the bar down and then back up. 1431 | dip = 6 1432 | 1433 | #: Dive the bar. 1434 | dive = 7 1435 | 1436 | #: Release the bar up. 1437 | releaseUp = 8 1438 | 1439 | #: Dip the bar up and then back down. 1440 | invertedDip = 9 1441 | 1442 | #: Return the bar. 1443 | return_ = 10 1444 | 1445 | #: Release the bar down. 1446 | releaseDown = 11 1447 | 1448 | 1449 | @hashableAttrs 1450 | class BendPoint: 1451 | """A single point within the BendEffect.""" 1452 | 1453 | position: int = 0 1454 | value: int = 0 1455 | vibrato: bool = False 1456 | 1457 | def getTime(self, duration): 1458 | """Gets the exact time when the point need to be played (MIDI). 1459 | 1460 | :param duration: the full duration of the effect. 1461 | """ 1462 | 1463 | return int(duration * self.position / BendEffect.maxPosition) 1464 | 1465 | 1466 | @hashableAttrs 1467 | class BendEffect: 1468 | """This effect is used to describe string bends and tremolo bars.""" 1469 | 1470 | type: BendType = BendType.none 1471 | value: int = 0 1472 | points: List[BendPoint] = attr.Factory(list) 1473 | 1474 | #: The note offset per bend point offset. 1475 | semitoneLength = 1 1476 | 1477 | #: The max position of the bend points (x axis) 1478 | maxPosition = 12 1479 | 1480 | #: The max value of the bend points (y axis) 1481 | maxValue = semitoneLength * 12 1482 | -------------------------------------------------------------------------------- /guitarpro/utils.py: -------------------------------------------------------------------------------- 1 | def clamp(iterable, length, fillvalue=None): 2 | """Set length of iterable to given length. 3 | 4 | If iterable is shorter then *length* then fill it with *fillvalue*, 5 | drop items otherwise. 6 | """ 7 | i = -1 8 | for i, x in enumerate(iterable): 9 | if i < length: 10 | yield x 11 | else: 12 | return 13 | for _ in range(i + 1, length): 14 | yield fillvalue 15 | -------------------------------------------------------------------------------- /playground/1 beat.tmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/playground/1 beat.tmp -------------------------------------------------------------------------------- /playground/1 whole bar and 2 beats.tmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/playground/1 whole bar and 2 beats.tmp -------------------------------------------------------------------------------- /playground/2 beats with lyrics.tmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/playground/2 beats with lyrics.tmp -------------------------------------------------------------------------------- /playground/2 beats.tmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/playground/2 beats.tmp -------------------------------------------------------------------------------- /playground/2 whole bars.tmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/playground/2 whole bars.tmp -------------------------------------------------------------------------------- /playground/4 beats with score info.tmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/playground/4 beats with score info.tmp -------------------------------------------------------------------------------- /playground/4 beats within bar.tmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/playground/4 beats within bar.tmp -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "PyGuitarPro" 3 | description = "Read, write, and manipulate GP3, GP4 and GP5 files." 4 | readme = "README.rst" 5 | license = {text = "LGPL-3.0-only"} 6 | authors = [{name = "Sviatoslav Abakumov", email = "dust.harvesting@gmail.com"}] 7 | classifiers = [ 8 | "Development Status :: 4 - Beta", 9 | "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", 10 | "Programming Language :: Python :: 3", 11 | "Programming Language :: Python :: 3 :: Only", 12 | "Programming Language :: Python :: 3.7", 13 | "Programming Language :: Python :: 3.8", 14 | "Programming Language :: Python :: 3.9", 15 | "Programming Language :: Python :: 3.10", 16 | "Programming Language :: Python :: 3.11", 17 | "Programming Language :: Python :: Implementation :: CPython", 18 | "Programming Language :: Python :: Implementation :: PyPy", 19 | "Topic :: Artistic Software", 20 | "Topic :: Multimedia :: Sound/Audio", 21 | ] 22 | requires-python = ">=3.7" 23 | dependencies = [ 24 | "attrs>=19.2", 25 | ] 26 | dynamic = ["version"] 27 | 28 | [project.urls] 29 | Documentation = "https://pyguitarpro.readthedocs.io/" 30 | "Source code" = "https://github.com/Perlence/PyGuitarPro" 31 | "Issue tracker" = "https://github.com/Perlence/PyGuitarPro/issues" 32 | 33 | [build-system] 34 | requires = ["setuptools"] 35 | build-backend = "setuptools.build_meta" 36 | 37 | [tool.setuptools] 38 | packages = ["guitarpro"] 39 | 40 | [tool.setuptools.dynamic] 41 | version = {attr = "guitarpro.__version__"} 42 | 43 | [tool.pytest.ini_options] 44 | testpaths = ["tests"] 45 | -------------------------------------------------------------------------------- /tests/001_Funky_Guy.gp5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/tests/001_Funky_Guy.gp5 -------------------------------------------------------------------------------- /tests/2 whole bars.tmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/tests/2 whole bars.tmp -------------------------------------------------------------------------------- /tests/Chords.gp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/tests/Chords.gp3 -------------------------------------------------------------------------------- /tests/Chords.gp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/tests/Chords.gp4 -------------------------------------------------------------------------------- /tests/Chords.gp5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/tests/Chords.gp5 -------------------------------------------------------------------------------- /tests/Demo v5.gp5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/tests/Demo v5.gp5 -------------------------------------------------------------------------------- /tests/Directions.gp5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/tests/Directions.gp5 -------------------------------------------------------------------------------- /tests/Duration.gp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/tests/Duration.gp3 -------------------------------------------------------------------------------- /tests/Effects.gp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/tests/Effects.gp3 -------------------------------------------------------------------------------- /tests/Effects.gp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/tests/Effects.gp4 -------------------------------------------------------------------------------- /tests/Effects.gp5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/tests/Effects.gp5 -------------------------------------------------------------------------------- /tests/Harmonics.gp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/tests/Harmonics.gp3 -------------------------------------------------------------------------------- /tests/Harmonics.gp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/tests/Harmonics.gp4 -------------------------------------------------------------------------------- /tests/Harmonics.gp5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/tests/Harmonics.gp5 -------------------------------------------------------------------------------- /tests/Key.gp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/tests/Key.gp4 -------------------------------------------------------------------------------- /tests/Key.gp5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/tests/Key.gp5 -------------------------------------------------------------------------------- /tests/Measure Header.gp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/tests/Measure Header.gp3 -------------------------------------------------------------------------------- /tests/Measure Header.gp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/tests/Measure Header.gp4 -------------------------------------------------------------------------------- /tests/Measure Header.gp5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/tests/Measure Header.gp5 -------------------------------------------------------------------------------- /tests/No Wah.gp5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/tests/No Wah.gp5 -------------------------------------------------------------------------------- /tests/RSE.gp5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/tests/RSE.gp5 -------------------------------------------------------------------------------- /tests/Repeat.gp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/tests/Repeat.gp4 -------------------------------------------------------------------------------- /tests/Repeat.gp5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/tests/Repeat.gp5 -------------------------------------------------------------------------------- /tests/Slides.gp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/tests/Slides.gp4 -------------------------------------------------------------------------------- /tests/Slides.gp5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/tests/Slides.gp5 -------------------------------------------------------------------------------- /tests/Strokes.gp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/tests/Strokes.gp4 -------------------------------------------------------------------------------- /tests/Strokes.gp5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/tests/Strokes.gp5 -------------------------------------------------------------------------------- /tests/Unknown Chord Extension.gp5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/tests/Unknown Chord Extension.gp5 -------------------------------------------------------------------------------- /tests/Unknown-m.gp5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/tests/Unknown-m.gp5 -------------------------------------------------------------------------------- /tests/Unknown.gp5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/tests/Unknown.gp5 -------------------------------------------------------------------------------- /tests/Vibrato.gp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/tests/Vibrato.gp4 -------------------------------------------------------------------------------- /tests/Voices.gp5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/tests/Voices.gp5 -------------------------------------------------------------------------------- /tests/Wah-m.gp5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/tests/Wah-m.gp5 -------------------------------------------------------------------------------- /tests/Wah.gp5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/tests/Wah.gp5 -------------------------------------------------------------------------------- /tests/chord_without_notes.gp5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Perlence/PyGuitarPro/d8644bb2d54ba3584068d9c8bd2ab095b334e361/tests/chord_without_notes.gp5 -------------------------------------------------------------------------------- /tests/test_conversion.py: -------------------------------------------------------------------------------- 1 | import io 2 | from pathlib import Path 3 | 4 | import pytest 5 | 6 | import guitarpro as gp 7 | 8 | 9 | LOCATION = Path(__file__).parent 10 | TESTS = [ 11 | 'Effects.gp3', 12 | 'Chords.gp3', 13 | 'Duration.gp3', 14 | 'Harmonics.gp3', 15 | 'Measure Header.gp3', 16 | 17 | 'Effects.gp4', 18 | 'Vibrato.gp4', 19 | 'Chords.gp4', 20 | 'Slides.gp4', 21 | 'Harmonics.gp4', 22 | 'Key.gp4', 23 | 'Repeat.gp4', 24 | 'Strokes.gp4', 25 | 'Measure Header.gp4', 26 | 27 | 'Effects.gp5', 28 | 'Voices.gp5', 29 | 'Unknown-m.gp5', 30 | 'Harmonics.gp5', 31 | 'Wah-m.gp5', 32 | 'Chords.gp5', 33 | 'Slides.gp5', 34 | 'RSE.gp5', 35 | 'Repeat.gp5', 36 | 'Strokes.gp5', 37 | 'Measure Header.gp5', 38 | 'Demo v5.gp5', 39 | ] 40 | CONVERSION_TESTS = [ 41 | # ('Effects.gp3', 'gp4'), 42 | ('Duration.gp3', 'gp4'), 43 | 44 | ('Slides.gp4', 'gp5'), 45 | ('Vibrato.gp4', 'gp5'), 46 | 47 | ('Repeat.gp4', 'gp5'), 48 | ] 49 | 50 | 51 | @pytest.mark.parametrize('filename', TESTS) 52 | def testReadWriteEquals(tmpdir, filename): 53 | filepath = LOCATION / filename 54 | songA = gp.parse(filepath) 55 | destpath = str(tmpdir.join(filename)) 56 | gp.write(songA, destpath) 57 | songB = gp.parse(destpath) 58 | assert songA == songB 59 | assert hash(songA) == hash(songB) 60 | 61 | 62 | @pytest.mark.parametrize('source, targetExt', CONVERSION_TESTS) 63 | def testConversion(tmpdir, source, targetExt): 64 | sourcepath = LOCATION / source 65 | songA = gp.parse(sourcepath) 66 | songA.versionTuple = None # Remove the version so it's determined by the extension 67 | destpath = str(tmpdir.join(f'{source}.{targetExt}')) 68 | gp.write(songA, destpath) 69 | songB = gp.parse(destpath) 70 | assert songA == songB 71 | 72 | 73 | def testClipboard(tmpdir): 74 | filepath = LOCATION / '2 whole bars.tmp' 75 | songA = gp.parse(filepath) 76 | songA.clipboard = None 77 | destpath = str(tmpdir.join('2 whole bars.tmp.gp5')) 78 | gp.write(songA, destpath) 79 | songB = gp.parse(destpath) 80 | assert songA == songB 81 | 82 | 83 | def testEmpty(tmpdir): 84 | emptyA = gp.Song() 85 | destpath = str(tmpdir.join('Empty.gp5')) 86 | gp.write(emptyA, destpath, version=(5, 2, 0)) 87 | 88 | emptyB = gp.parse(destpath) 89 | assert emptyA == emptyB 90 | 91 | 92 | def testGuessVersion(tmpdir): 93 | filename = 'Effects.gp5' 94 | filepath = LOCATION / filename 95 | songA = gp.parse(filepath) 96 | songA.version = songA.versionTuple = None 97 | 98 | for ext, versionTuple in gp.io._EXT_VERSIONS.items(): 99 | if ext == 'tmp': 100 | continue 101 | destpath = str(tmpdir.join(filename + '.' + ext)) 102 | gp.write(songA, destpath) 103 | songB = gp.parse(destpath) 104 | assert songB.versionTuple == versionTuple 105 | 106 | 107 | @pytest.mark.parametrize('filename', [ 108 | 'chord_without_notes.gp5', 109 | '001_Funky_Guy.gp5', 110 | 'Unknown Chord Extension.gp5', 111 | ]) 112 | def testChord(tmpdir, caplog, filename): 113 | filepath = LOCATION / filename 114 | song = gp.parse(filepath) 115 | assert song.tracks[0].measures[0].voices[0].beats[0].effect.chord is not None 116 | 117 | destpath = str(tmpdir.join('no_chord_strings.gp5')) 118 | gp.write(song, destpath) 119 | if filename == 'Unknown Chord Extension.gp5': 120 | iobase_logs = [log for log in caplog.records if log.name == 'guitarpro.iobase'] 121 | [record] = iobase_logs 122 | assert 'is an unknown ChordExtension' in record.msg 123 | song2 = gp.parse(destpath) 124 | assert song == song2 125 | 126 | 127 | @pytest.mark.parametrize('version', ['gp3', 'gp4', 'gp5']) 128 | def testReadErrorAnnotation(version): 129 | def writeToBytesIO(song): 130 | stream = io.BytesIO() 131 | stream.name = f'percusion.{version}' 132 | gp.write(song, stream, encoding='cp1252') 133 | stream.seek(0) 134 | return stream 135 | 136 | song = gp.Song() 137 | song.tracks[0].name = "Percusión" 138 | stream = writeToBytesIO(song) 139 | # readMeasureHeader 140 | with pytest.raises(gp.GPException, match="reading track 1, got UnicodeDecodeError: "): 141 | gp.parse(stream, encoding='ascii') 142 | 143 | song = gp.Song() 144 | song.tracks[0].measures[0].marker = gp.Marker(title="Percusión") 145 | stream = writeToBytesIO(song) 146 | # readTracks 147 | with pytest.raises(gp.GPException, match="reading measure 1, got UnicodeDecodeError: "): 148 | gp.parse(stream, encoding='ascii') 149 | 150 | song = gp.Song() 151 | voice = song.tracks[0].measures[0].voices[0] 152 | beat = gp.Beat(voice, text="Percusión") 153 | voice.beats.append(beat) 154 | stream = writeToBytesIO(song) 155 | # readMeasures 156 | with pytest.raises(gp.GPException, match="reading track 1, measure 1, voice 1, beat 1, got UnicodeDecodeError: "): 157 | gp.parse(stream, encoding='ascii') 158 | 159 | 160 | @pytest.mark.parametrize('version', ['gp3', 'gp4', 'gp5']) 161 | def testWriteErrorAnnotation(version): 162 | fp = io.BytesIO() 163 | fp.name = f'beep.{version}' 164 | 165 | song = gp.Song() 166 | song.tracks[0].measures[0].timeSignature.numerator = 'nooo' 167 | # writeMeasureHeader 168 | with pytest.raises(gp.GPException, match="writing measure 1, got ValueError: invalid"): 169 | gp.write(song, fp) 170 | 171 | song = gp.Song() 172 | song.tracks[0].fretCount = 'nooo' 173 | # writeTracks 174 | with pytest.raises(gp.GPException, match="writing track 1, got ValueError: invalid"): 175 | gp.write(song, fp) 176 | 177 | song = gp.Song() 178 | voice = song.tracks[0].measures[0].voices[0] 179 | invalidBeat = gp.Beat(voice, status='nooo') 180 | voice.beats.append(invalidBeat) 181 | # writeMeasures 182 | with pytest.raises(gp.GPException, match="writing track 1, measure 1, voice 1, beat 1, got AttributeError: 'str'"): 183 | gp.write(song, fp) 184 | 185 | 186 | def testSongNewMeasure(tmpdir): 187 | song = gp.Song() 188 | song.newMeasure() 189 | filename = str(tmpdir.join('songNewMeasure.gp5')) 190 | gp.write(song, filename) 191 | song2 = gp.parse(filename) 192 | assert len(song2.measureHeaders) == 2 193 | assert len(song2.tracks[0].measures) == 2 194 | assert song == song2 195 | -------------------------------------------------------------------------------- /tests/test_models.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import guitarpro as gp 4 | 5 | 6 | def testHashable(): 7 | song = gp.Song() 8 | hash(song) 9 | 10 | anotherSong = gp.Song() 11 | assert song == anotherSong 12 | assert hash(song) == hash(anotherSong) 13 | 14 | coda = gp.DirectionSign('Coda') 15 | segno = gp.DirectionSign('Segno') 16 | assert coda != segno 17 | assert hash(coda) != hash(segno) 18 | 19 | 20 | @pytest.mark.parametrize('value', [1, 2, 4, 8, 16, 32, 64]) 21 | @pytest.mark.parametrize('isDotted', [False, True]) 22 | @pytest.mark.parametrize('tuplet', gp.Tuplet.supportedTuplets) 23 | def testDuration(value, isDotted, tuplet): 24 | dur = gp.Duration(value, isDotted=isDotted, tuplet=gp.Tuplet(*tuplet)) 25 | time = dur.time 26 | newDur = gp.Duration.fromTime(time) 27 | assert isinstance(newDur.value, int) 28 | assert time == newDur.time 29 | 30 | 31 | def testGraceDuration(): 32 | g16 = gp.GraceEffect(duration=16) 33 | assert g16.durationTime == 240 34 | g32 = gp.GraceEffect(duration=32) 35 | assert g32.durationTime == 120 36 | g64 = gp.GraceEffect(duration=64) 37 | assert g64.durationTime == 60 38 | 39 | 40 | def testBeatStartInMeasure(): 41 | song = gp.Song() 42 | measure = song.tracks[0].measures[0] 43 | voice = measure.voices[0] 44 | beat = gp.Beat(voice, start=measure.start) 45 | beat2 = gp.Beat(voice, start=measure.start + beat.duration.time) 46 | voice.beats.append(beat) 47 | assert beat.startInMeasure == 0 48 | assert beat2.startInMeasure == 960 49 | 50 | with pytest.raises(AttributeError): 51 | beat2.realStart 52 | 53 | 54 | def testGuitarString(): 55 | assert str(gp.GuitarString(number=1, value=0)) == 'C-1' 56 | assert str(gp.GuitarString(number=1, value=40)) == 'E2' 57 | assert str(gp.GuitarString(number=1, value=64)) == 'E4' 58 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py3{12,11,10,9,8,7}, 4 | pypy3{9}, 5 | flake8 6 | 7 | [testenv] 8 | deps=pytest 9 | commands=py.test 10 | 11 | [flake8] 12 | max-line-length=120 13 | 14 | [testenv:flake8] 15 | basepython=python 16 | extras=lint 17 | deps=flake8 18 | commands=flake8 {toxinidir}/guitarpro {toxinidir}/tests 19 | --------------------------------------------------------------------------------