├── py.typed ├── tests ├── __init__.py ├── data │ └── .gitkeep ├── elements │ ├── test_neck_dots.py │ ├── test_tuning.py │ ├── test_fret_number.py │ ├── test_nut.py │ ├── test_frets.py │ ├── test_strings.py │ ├── test_background.py │ └── test_notes.py ├── test_notes_creator.py ├── test_exporters.py ├── test_notes.py ├── test_utils.py └── test_e2e.py ├── docs ├── scripts │ ├── __init__.py │ ├── plot.py │ └── default.mplstyle ├── source │ ├── _static │ │ └── css │ │ │ └── custom.css │ ├── get-started │ │ ├── index.md │ │ └── get-started.md │ ├── assets │ │ ├── black_logo@2x.png │ │ ├── white_logo@2x.png │ │ ├── black_logo_square@2x.png │ │ ├── white_logo_square@2x.png │ │ ├── c_major_chord.svg │ │ ├── C_M │ │ │ ├── C_M_position_0.svg │ │ │ ├── C_M_position_1.svg │ │ │ ├── C_M_position_3.svg │ │ │ └── C_M_position_2.svg │ │ └── A_Minorpentatonic │ │ │ ├── A_Minorpentatonic_position_6.svg │ │ │ ├── A_Minorpentatonic_position_3.svg │ │ │ ├── A_Minorpentatonic_position_1.svg │ │ │ ├── A_Minorpentatonic_position_4.svg │ │ │ ├── A_Minorpentatonic_position_0.svg │ │ │ ├── A_Minorpentatonic_position_2.svg │ │ │ └── A_Minorpentatonic_position_5.svg │ ├── api_documentation │ │ ├── utils.md │ │ ├── constants.md │ │ ├── fretboard.md │ │ ├── note_colors.md │ │ ├── notes_creators.md │ │ ├── exporters.md │ │ ├── converters.md │ │ ├── index.md │ │ ├── fretboards.md │ │ └── elements.md │ ├── index.md │ └── conf.py ├── Makefile └── make.bat ├── fretboardgtr ├── elements │ ├── __init__.py │ ├── base.py │ ├── nut.py │ ├── frets.py │ ├── neck_dots.py │ ├── background.py │ ├── cross.py │ ├── strings.py │ ├── tuning.py │ ├── fret_number.py │ └── notes.py ├── fretboards │ ├── __init__.py │ ├── converters.py │ ├── base.py │ ├── elements.py │ ├── config.py │ ├── vertical.py │ └── horizontal.py ├── _version.py ├── base.py ├── __init__.py ├── note_colors.py ├── exporters.py ├── notes.py └── notes_creators.py ├── .pydocstyle.ini ├── MANIFEST.in ├── setup.py ├── pytest.ini ├── .gitattributes ├── Dockerfile ├── .editorconfig ├── .flake8 ├── .coveragerc ├── test.py ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── PULL_REQUEST_TEMPLATE │ └── pull_request_template.md └── workflows │ └── ci.yaml ├── .readthedocs.yaml ├── .pre-commit-config.yaml ├── .gitignore ├── README.md ├── pyproject.toml ├── cliff.toml ├── CHANGELOG.md └── CONTRIBUTING.md /py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/scripts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fretboardgtr/elements/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fretboardgtr/fretboards/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fretboardgtr/_version.py: -------------------------------------------------------------------------------- 1 | version_str = "0.2.7" 2 | version_tuple = version_str.split(".") 3 | -------------------------------------------------------------------------------- /docs/source/_static/css/custom.css: -------------------------------------------------------------------------------- 1 | .navbar-brand { 2 | max-width: 150px !important; 3 | margin: auto; 4 | } 5 | -------------------------------------------------------------------------------- /docs/source/get-started/index.md: -------------------------------------------------------------------------------- 1 | ```{toctree} 2 | :maxdepth: 2 3 | 4 | ./get-started.md 5 | ./configuration.md 6 | ``` 7 | -------------------------------------------------------------------------------- /docs/source/assets/black_logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antscloud/fretboardgtr/HEAD/docs/source/assets/black_logo@2x.png -------------------------------------------------------------------------------- /docs/source/assets/white_logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antscloud/fretboardgtr/HEAD/docs/source/assets/white_logo@2x.png -------------------------------------------------------------------------------- /.pydocstyle.ini: -------------------------------------------------------------------------------- 1 | [pydocstyle] 2 | convention=numpy 3 | # Deactivate all the missing docstrings 4 | add_ignore = D1 5 | match = .*\.py 6 | -------------------------------------------------------------------------------- /docs/source/assets/black_logo_square@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antscloud/fretboardgtr/HEAD/docs/source/assets/black_logo_square@2x.png -------------------------------------------------------------------------------- /docs/source/assets/white_logo_square@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antscloud/fretboardgtr/HEAD/docs/source/assets/white_logo_square@2x.png -------------------------------------------------------------------------------- /docs/source/api_documentation/utils.md: -------------------------------------------------------------------------------- 1 | # Utils 2 | 3 | ```{eval-rst} 4 | .. automodule:: fretboardgtr.utils 5 | :members: 6 | :undoc-members: 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/source/api_documentation/constants.md: -------------------------------------------------------------------------------- 1 | # Constants 2 | 3 | ```{eval-rst} 4 | .. automodule:: fretboardgtr.constants 5 | :members: 6 | :undoc-members: 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/source/api_documentation/fretboard.md: -------------------------------------------------------------------------------- 1 | # Fretboard 2 | 3 | ```{eval-rst} 4 | .. automodule:: fretboardgtr.fretboard 5 | :members: 6 | :undoc-members: 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/source/api_documentation/note_colors.md: -------------------------------------------------------------------------------- 1 | # Note Colors 2 | 3 | ```{eval-rst} 4 | .. automodule:: fretboardgtr.note_colors 5 | :members: 6 | :undoc-members: 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/source/api_documentation/notes_creators.md: -------------------------------------------------------------------------------- 1 | # Notes Creators 2 | 3 | ```{eval-rst} 4 | .. automodule:: fretboardgtr.notes_creators 5 | :members: 6 | :undoc-members: 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/source/api_documentation/exporters.md: -------------------------------------------------------------------------------- 1 | # Exporters 2 | 3 | 4 | ```{eval-rst} 5 | .. automodule:: fretboardgtr.exporters 6 | :members: 7 | :undoc-members: 8 | 9 | ``` 10 | -------------------------------------------------------------------------------- /docs/source/api_documentation/converters.md: -------------------------------------------------------------------------------- 1 | # Converters 2 | 3 | 4 | ```{eval-rst} 5 | .. automodule:: fretboardgtr.fretboards.converters 6 | :members: 7 | :undoc-members: 8 | 9 | ``` 10 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CONTRIBUTING.md 2 | include LICENSE 3 | include README.md 4 | 5 | recursive-include tests * 6 | recursive-exclude * __pycache__ 7 | recursive-exclude * *.py[co] 8 | 9 | recursive-include docs *.rst *.md conf.py Makefile make.bat *.jpg *.png *.gif 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """The setup script.""" 2 | from typing import Dict 3 | 4 | from setuptools import setup 5 | 6 | version: Dict[str, str] = {} 7 | with open("fretboardgtr/_version.py") as fp: 8 | exec(fp.read(), version) 9 | 10 | setup(version=version["version_str"]) 11 | -------------------------------------------------------------------------------- /docs/source/api_documentation/index.md: -------------------------------------------------------------------------------- 1 | # API Documentation 2 | 3 | ```{toctree} 4 | :maxdepth: 2 5 | 6 | ./elements.md 7 | ./fretboard.md 8 | ./fretboards.md 9 | ./exporters.md 10 | ./converters.md 11 | ./notes_creators.md 12 | ./note_colors.md 13 | ./constants.md 14 | ./utils.md 15 | ``` 16 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | 3 | addopts = 4 | -vv 5 | --cov-config=.coveragerc 6 | --cov-report html:coverage/html 7 | --cov-report xml:coverage/coverage.xml 8 | --cov-report term 9 | --cov=. 10 | testpaths = fretboardgtr/tests 11 | filterwarnings = 12 | ignore::DeprecationWarning 13 | default:::fretboardgtr.* 14 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Add to LFS all the tests data files 2 | fretboardgtr/tests/data/** filter=lfs diff=lfs merge=lfs -text 3 | tests/data/** filter=lfs diff=lfs merge=lfs -text 4 | fretboardgtr/test/data/** filter=lfs diff=lfs merge=lfs -text 5 | test/data/** filter=lfs diff=lfs merge=lfs -text 6 | 7 | # Add to LFS all nc files 8 | +*.nc filter=lfs diff=lfs merge=lfs -text 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | FROM python:3.10-bullseye 3 | 4 | LABEL description="fretboardgtr docker image" 5 | 6 | RUN apt-get update \ 7 | && apt-get install -y --no-install-recommends libudunits2-dev gdb \ 8 | && rm -rf /var/lib/apt/lists/* 9 | 10 | RUN pip install --upgrade pip 11 | 12 | COPY . /fretboardgtr/ 13 | 14 | RUN pip install /fretboardgtr[dev] 15 | -------------------------------------------------------------------------------- /docs/source/api_documentation/fretboards.md: -------------------------------------------------------------------------------- 1 | 2 | # Fretboards 3 | 4 | ## Horizontal Fretboard 5 | 6 | ```{eval-rst} 7 | .. automodule:: fretboardgtr.fretboards.horizontal 8 | :members: 9 | :undoc-members: 10 | ``` 11 | 12 | 13 | ## Vertical Fretboard 14 | 15 | ```{eval-rst} 16 | .. automodule:: fretboardgtr.fretboards.vertical 17 | :members: 18 | :undoc-members: 19 | ``` 20 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.bat] 14 | indent_style = tab 15 | end_of_line = crlf 16 | 17 | [LICENSE] 18 | insert_final_newline = false 19 | 20 | [Makefile] 21 | indent_style = tab 22 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | # E402 Module level import not at top of file 3 | # E203 Whitespace before ':' 4 | # E203 Issue : https://github.com/PyCQA/pycodestyle/issues/373 5 | 6 | extend-ignore = E402,E203 7 | # Ignore unused imports in __init__ 8 | per-file-ignores = __init__.py:F401 9 | exclude = .git,__pycache__,docs/source/conf.py,old,build,dist 10 | max-complexity = 15 11 | max-line-length = 88 12 | -------------------------------------------------------------------------------- /fretboardgtr/elements/base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | import svgwrite 4 | 5 | 6 | class FretBoardElement(ABC): 7 | """Interface to implement to define a FretBoard element. 8 | 9 | This simply consists of converting the element to a svgwrite element 10 | """ 11 | 12 | @abstractmethod 13 | def get_svg(self) -> svgwrite.base.BaseElement: 14 | pass 15 | -------------------------------------------------------------------------------- /docs/source/index.md: -------------------------------------------------------------------------------- 1 | # Welcome to FretBoardGtr Documentation 2 | 3 | Package that make easy creation of **highly customizable** fretboards and chords diagrams. 4 | 5 | ## Table of content 6 | 7 | ```{toctree} 8 | :maxdepth: 2 9 | :caption: Guides 10 | 11 | get-started/index.md 12 | ``` 13 | 14 | ```{toctree} 15 | :maxdepth: 2 16 | :caption: References 17 | 18 | api_documentation/index.md 19 | ``` 20 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit= 3 | fretboardgtr/tests/* ,\ 4 | ./tests/* ,\ 5 | setup.py 6 | [report] 7 | exclude_lines = 8 | pragma: no cover 9 | def __repr__ 10 | if self.debug: 11 | if settings.DEBUG 12 | raise AssertionError 13 | raise NotImplementedError 14 | if 0: 15 | if __name__ == .__main__.: 16 | class .*\bProtocol\): 17 | @(abc\.)?abstractmethod 18 | except ImportError: 19 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | from fretboardgtr import FretBoard 2 | from fretboardgtr.notes_creators import ScaleFromName 3 | 4 | TUNING = ["E", "A", "D", "G", "B", "E"] 5 | config = { 6 | "general": { 7 | "first_fret": 0, 8 | "last_fret": 16, 9 | "fret_width": 50, 10 | "show_note_name": True, 11 | "show_degree_name": False, 12 | } 13 | } 14 | fretboard = FretBoard(config=config) 15 | c_scale = ScaleFromName(root="C", mode="Ionian").build().get_scale(TUNING) 16 | c_scale[-1] = [] 17 | fretboard.add_scale(c_scale, root="C") 18 | fretboard.export("c_scale.svg", format="svg") 19 | -------------------------------------------------------------------------------- /tests/elements/test_neck_dots.py: -------------------------------------------------------------------------------- 1 | from fretboardgtr.elements.neck_dots import NeckDot, NeckDotConfig 2 | 3 | 4 | def test_neck_dots_get_svg(): 5 | neck_dot = NeckDot(position=(0.0, 0.0)) 6 | attribs = neck_dot.get_svg().attribs 7 | assert attribs["cx"] == 0.0 8 | assert attribs["cy"] == 0.0 9 | 10 | 11 | def test_neck_dots_get_svg_custom_config(): 12 | neck_dot_config = NeckDotConfig(radius=30) 13 | neck_dot = NeckDot(position=(0.0, 0.0), config=neck_dot_config) 14 | attribs = neck_dot.get_svg().attribs 15 | assert attribs["cx"] == 0.0 16 | assert attribs["cy"] == 0.0 17 | assert attribs["r"] == 30 18 | -------------------------------------------------------------------------------- /tests/elements/test_tuning.py: -------------------------------------------------------------------------------- 1 | from fretboardgtr.elements.tuning import Tuning, TuningConfig 2 | 3 | 4 | def test_tuning_get_svg(): 5 | tuning = Tuning("test", position=(0.0, 0.0)) 6 | attribs = tuning.get_svg().attribs 7 | assert float(attribs["x"]) == 0.0 8 | assert float(attribs["y"]) == 0.0 9 | 10 | 11 | def test_tuning_get_svg_custom_config(): 12 | tuning_config = TuningConfig(fontsize=30) 13 | tuning = Tuning("test", position=(0.0, 0.0), config=tuning_config) 14 | attribs = tuning.get_svg().attribs 15 | assert float(attribs["x"]) == 0.0 16 | assert float(attribs["y"]) == 0.0 17 | assert attribs["font-size"] == 30 18 | -------------------------------------------------------------------------------- /docs/scripts/plot.py: -------------------------------------------------------------------------------- 1 | """Script allowing creation of plot when building the docs. 2 | 3 | You can then access to your plot through the docs. For doing this, the 4 | make_plots function is called in the conf.py directory. 5 | """ 6 | import os 7 | 8 | BASE_DIR = os.path.dirname(__file__) 9 | EXPORT_FOLDER = os.path.join(BASE_DIR, "..", "source", "assets", "plots") 10 | 11 | 12 | def doc_plot() -> None: 13 | """doc_plot Generate plot when building docs. 14 | 15 | Save it in EXPORT_FOLDER. 16 | """ 17 | ... 18 | 19 | 20 | def make_plots() -> None: 21 | os.makedirs(EXPORT_FOLDER, exist_ok=True) 22 | doc_plot() 23 | 24 | 25 | if __name__ == "__main__": 26 | make_plots() 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /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 = source 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 | -------------------------------------------------------------------------------- /tests/elements/test_fret_number.py: -------------------------------------------------------------------------------- 1 | from fretboardgtr.elements.fret_number import FretNumber, FretNumberConfig 2 | 3 | 4 | def test_fret_number_get_svg(): 5 | fret_number = FretNumber("test", position=(0.0, 0.0)) 6 | attribs = fret_number.get_svg().attribs 7 | assert float(attribs["x"]) == 0.0 8 | assert float(attribs["y"]) == 0.0 9 | 10 | 11 | def test_fret_number_get_svg_custom_config(): 12 | fret_number_config = FretNumberConfig(fontsize=30) 13 | fret_number = FretNumber("test", position=(0.0, 0.0), config=fret_number_config) 14 | attribs = fret_number.get_svg().attribs 15 | assert fret_number.get_svg().text == "test" 16 | assert float(attribs["x"]) == 0.0 17 | assert float(attribs["y"]) == 0.0 18 | assert attribs["font-size"] == 30 19 | -------------------------------------------------------------------------------- /tests/elements/test_nut.py: -------------------------------------------------------------------------------- 1 | from fretboardgtr.elements.nut import Nut, NutConfig 2 | 3 | 4 | def test_nut_get_svg(): 5 | nut = Nut(start_position=(0.0, 0.0), end_position=(10.0, 10.0)) 6 | attribs = nut.get_svg().attribs 7 | print(attribs) 8 | assert attribs["x1"] == 0.0 9 | assert attribs["y1"] == 0.0 10 | assert attribs["x2"] == 10.0 11 | assert attribs["y2"] == 10.0 12 | 13 | 14 | def test_nut_get_svg_custom_config(): 15 | nut_config = NutConfig(color="blue") 16 | nut = Nut(start_position=(0.0, 0.0), end_position=(10.0, 10.0), config=nut_config) 17 | attribs = nut.get_svg().attribs 18 | assert attribs["x1"] == 0.0 19 | assert attribs["y1"] == 0.0 20 | assert attribs["x2"] == 10.0 21 | assert attribs["y2"] == 10.0 22 | assert attribs["stroke"] == "blue" 23 | -------------------------------------------------------------------------------- /tests/elements/test_frets.py: -------------------------------------------------------------------------------- 1 | from fretboardgtr.elements.frets import Fret, FretConfig 2 | 3 | 4 | def test_fret_get_svg(): 5 | fret = Fret(start_position=(0.0, 0.0), end_position=(10.0, 10.0)) 6 | attribs = fret.get_svg().attribs 7 | print(attribs) 8 | assert attribs["x1"] == 0.0 9 | assert attribs["y1"] == 0.0 10 | assert attribs["x2"] == 10.0 11 | assert attribs["y2"] == 10.0 12 | 13 | 14 | def test_fret_get_svg_custom_config(): 15 | fret_config = FretConfig(color="blue") 16 | fret = Fret( 17 | start_position=(0.0, 0.0), end_position=(10.0, 10.0), config=fret_config 18 | ) 19 | attribs = fret.get_svg().attribs 20 | assert attribs["x1"] == 0.0 21 | assert attribs["y1"] == 0.0 22 | assert attribs["x2"] == 10.0 23 | assert attribs["y2"] == 10.0 24 | assert attribs["stroke"] == "blue" 25 | -------------------------------------------------------------------------------- /tests/elements/test_strings.py: -------------------------------------------------------------------------------- 1 | from fretboardgtr.elements.strings import String, StringConfig 2 | 3 | 4 | def test_string_get_svg(): 5 | string = String(start_position=(0.0, 0.0), end_position=(10.0, 10.0)) 6 | attribs = string.get_svg().attribs 7 | print(attribs) 8 | assert attribs["x1"] == 0.0 9 | assert attribs["y1"] == 0.0 10 | assert attribs["x2"] == 10.0 11 | assert attribs["y2"] == 10.0 12 | 13 | 14 | def test_string_get_svg_custom_config(): 15 | string_config = StringConfig(color="blue") 16 | string = String( 17 | start_position=(0.0, 0.0), end_position=(10.0, 10.0), config=string_config 18 | ) 19 | attribs = string.get_svg().attribs 20 | assert attribs["x1"] == 0.0 21 | assert attribs["y1"] == 0.0 22 | assert attribs["x2"] == 10.0 23 | assert attribs["y2"] == 10.0 24 | assert attribs["stroke"] == "blue" 25 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-20.04 11 | tools: 12 | python: "3.10" 13 | # You can also specify other tool versions: 14 | # nodejs: "16" 15 | # rust: "1.55" 16 | # golang: "1.17" 17 | 18 | # Build documentation in the docs/ directory with Sphinx 19 | sphinx: 20 | configuration: docs/source/conf.py 21 | 22 | # If using Sphinx, optionally build your docs in additional formats such as PDF 23 | # formats: 24 | # - pdf 25 | 26 | # Optionally declare the Python requirements required to build your docs 27 | python: 28 | install: 29 | - method: pip 30 | path: . 31 | extra_requirements: 32 | - dev 33 | -------------------------------------------------------------------------------- /tests/elements/test_background.py: -------------------------------------------------------------------------------- 1 | from fretboardgtr.elements.background import Background, BackgroundConfig 2 | 3 | 4 | def test_background_get_svg(): 5 | background = Background(position=(0.0, 0.0), size=(10.0, 10.0)) 6 | attribs = background.get_svg().attribs 7 | assert attribs["x"] == 0.0 8 | assert attribs["y"] == 0.0 9 | assert attribs["width"] == 10.0 10 | assert attribs["height"] == 10.0 11 | 12 | 13 | def test_background_get_svg_custom_config(): 14 | background_config = BackgroundConfig(color="blue") 15 | background = Background( 16 | position=(0.0, 0.0), size=(10.0, 10.0), config=background_config 17 | ) 18 | attribs = background.get_svg().attribs 19 | assert attribs["x"] == 0.0 20 | assert attribs["y"] == 0.0 21 | assert attribs["width"] == 10.0 22 | assert attribs["height"] == 10.0 23 | assert attribs["fill"] == "blue" 24 | -------------------------------------------------------------------------------- /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=source 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 | -------------------------------------------------------------------------------- /tests/test_notes_creator.py: -------------------------------------------------------------------------------- 1 | from fretboardgtr.notes_creators import ChordFromName, ScaleFromName 2 | 3 | 4 | def test_scale_creator(): 5 | scale = ScaleFromName(root="C", mode="Ionian").build() 6 | assert scale.notes == ["C", "D", "E", "F", "G", "A", "B"] 7 | 8 | 9 | def test_chord_creator(): 10 | chord = ChordFromName(root="C", quality="M").build() 11 | assert chord.notes == ["C", "E", "G"] 12 | 13 | 14 | def test_chord_creator_fingerings(): 15 | fingerings = ( 16 | ChordFromName(root="C", quality="M") 17 | .build() 18 | .get_chord_fingerings(["E", "A", "D", "G", "B", "E"]) 19 | ) 20 | assert len(fingerings) > 1000 21 | 22 | 23 | def test_scale_creator_position(): 24 | scale_positions = ( 25 | ScaleFromName(root="C", mode="Ionian") 26 | .build() 27 | .get_scale_positions(["E", "A", "D", "G", "B", "E"]) 28 | ) 29 | assert len(scale_positions) > 5 30 | -------------------------------------------------------------------------------- /fretboardgtr/base.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Any, Dict 3 | 4 | 5 | class ConfigIniter: 6 | """Mixin class that define methods read the different configurations. 7 | 8 | All the components/elements configuration must subclass this Mixin. 9 | Then, all the configuration can be read thank to the method (eg 10 | from_dict) recursively 11 | """ 12 | 13 | @classmethod 14 | def from_dict(cls, _dict: Dict) -> Any: 15 | kwargs = {} 16 | for arg in _dict: 17 | if arg not in cls.__annotations__.keys(): 18 | logging.warning( 19 | f'"{arg}" key confuration was not found in {cls.__name__}' 20 | ) 21 | continue 22 | _type = cls.__annotations__[arg] 23 | if hasattr(_type, "from_dict"): 24 | kwargs[arg] = _type.from_dict(_dict[arg]) 25 | else: 26 | kwargs[arg] = _dict[arg] 27 | return cls(**kwargs) 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 1. Go to '...' 15 | 2. Click on '....' 16 | 3. Scroll down to '....' 17 | 4. See error 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **Desktop (please complete the following information):** 26 | - OS: [e.g. iOS] 27 | - Browser [e.g. chrome, safari] 28 | - Version [e.g. 22] 29 | 30 | **Smartphone (please complete the following information):** 31 | - Device: [e.g. iPhone6] 32 | - OS: [e.g. iOS8.1] 33 | - Browser [e.g. stock browser, safari] 34 | - Version [e.g. 22] 35 | 36 | **Additional context** 37 | Add any other context about the problem here. 38 | -------------------------------------------------------------------------------- /fretboardgtr/__init__.py: -------------------------------------------------------------------------------- 1 | """Top-level package for FretBoardGtr.""" 2 | from fretboardgtr._version import version_str 3 | from fretboardgtr.elements.background import Background, BackgroundConfig 4 | from fretboardgtr.elements.fret_number import FretNumber, FretNumberConfig 5 | from fretboardgtr.elements.frets import Fret, FretConfig 6 | from fretboardgtr.elements.neck_dots import NeckDot, NeckDotConfig 7 | from fretboardgtr.elements.notes import ( 8 | FrettedNote, 9 | FrettedNoteConfig, 10 | OpenNote, 11 | OpenNoteConfig, 12 | ) 13 | from fretboardgtr.elements.nut import Nut, NutConfig 14 | from fretboardgtr.elements.strings import String, StringConfig 15 | from fretboardgtr.elements.tuning import Tuning, TuningConfig 16 | from fretboardgtr.fretboard import FretBoard 17 | from fretboardgtr.fretboards.base import FretBoardLike 18 | from fretboardgtr.fretboards.config import FretBoardConfig, FretBoardGeneralConfig 19 | from fretboardgtr.fretboards.converters import FretBoardToSVGConverter 20 | from fretboardgtr.fretboards.elements import FretBoardElements 21 | from fretboardgtr.note_colors import NoteColors 22 | from fretboardgtr.notes_creators import NotesContainer 23 | 24 | __author__ = "Antoine Gibek" 25 | __email__ = "antoine.gibek@gmail.com" 26 | __version__ = version_str 27 | -------------------------------------------------------------------------------- /docs/source/api_documentation/elements.md: -------------------------------------------------------------------------------- 1 | # Elements 2 | 3 | ## Background 4 | 5 | ```{eval-rst} 6 | .. automodule:: fretboardgtr.elements.background 7 | :members: 8 | :undoc-members: 9 | ``` 10 | 11 | ## Frets 12 | 13 | ```{eval-rst} 14 | .. automodule:: fretboardgtr.elements.frets 15 | :members: 16 | :undoc-members: 17 | ``` 18 | 19 | ## Fret numbers 20 | 21 | ```{eval-rst} 22 | .. automodule:: fretboardgtr.elements.fret_number 23 | :members: 24 | :undoc-members: 25 | ``` 26 | 27 | ## Neck Dots 28 | 29 | ```{eval-rst} 30 | .. automodule:: fretboardgtr.elements.neck_dots 31 | :members: 32 | :undoc-members: 33 | ``` 34 | 35 | ## Nut 36 | 37 | ```{eval-rst} 38 | .. automodule:: fretboardgtr.elements.nut 39 | :members: 40 | :undoc-members: 41 | ``` 42 | 43 | ## String 44 | 45 | ```{eval-rst} 46 | .. automodule:: fretboardgtr.elements.strings 47 | :members: 48 | :undoc-members: 49 | ``` 50 | 51 | ## Tuning 52 | 53 | ```{eval-rst} 54 | .. automodule:: fretboardgtr.elements.tuning 55 | :members: 56 | :undoc-members: 57 | ``` 58 | 59 | ## Cross 60 | 61 | ```{eval-rst} 62 | .. automodule:: fretboardgtr.elements.cross 63 | :members: 64 | :undoc-members: 65 | ``` 66 | ## Notes 67 | 68 | ```{eval-rst} 69 | .. automodule:: fretboardgtr.elements.notes 70 | :members: 71 | :undoc-members: 72 | ``` 73 | -------------------------------------------------------------------------------- /fretboardgtr/elements/nut.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional, Tuple 3 | 4 | import svgwrite 5 | 6 | from fretboardgtr.base import ConfigIniter 7 | from fretboardgtr.constants import BLACK 8 | from fretboardgtr.elements.base import FretBoardElement 9 | 10 | SVG_OVERLAY = 10 # overlay 11 | 12 | 13 | @dataclass 14 | class NutConfig(ConfigIniter): 15 | """Nut element configuration.""" 16 | 17 | color: str = BLACK 18 | width: int = 6 19 | 20 | 21 | class Nut(FretBoardElement): 22 | """Nut element to be drawn in the final fretboard.""" 23 | 24 | def __init__( 25 | self, 26 | start_position: Tuple[float, float], 27 | end_position: Tuple[float, float], 28 | config: Optional[NutConfig] = None, 29 | ): 30 | self.config = config if config else NutConfig() 31 | self.start_position = start_position 32 | self.end_position = end_position 33 | 34 | def get_svg(self) -> svgwrite.base.BaseElement: 35 | """Convert the Nut to a svgwrite object. 36 | 37 | This maps the NutConfig configuration attributes to the svg 38 | attributes 39 | """ 40 | line = svgwrite.shapes.Line( 41 | start=self.start_position, 42 | end=self.end_position, 43 | stroke=self.config.color, 44 | stroke_width=self.config.width, 45 | ) 46 | return line 47 | -------------------------------------------------------------------------------- /fretboardgtr/elements/frets.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional, Tuple 3 | 4 | import svgwrite 5 | 6 | from fretboardgtr.base import ConfigIniter 7 | from fretboardgtr.constants import GRAY 8 | from fretboardgtr.elements.base import FretBoardElement 9 | 10 | SVG_OVERLAY = 10 # overlay 11 | 12 | 13 | @dataclass 14 | class FretConfig(ConfigIniter): 15 | """Frets element configuration.""" 16 | 17 | color: str = GRAY 18 | width: int = 3 19 | 20 | 21 | class Fret(FretBoardElement): 22 | """Fret element to be drawn in the final fretboard.""" 23 | 24 | def __init__( 25 | self, 26 | start_position: Tuple[float, float], 27 | end_position: Tuple[float, float], 28 | config: Optional[FretConfig] = None, 29 | ): 30 | self.config = config if config else FretConfig() 31 | self.start_position = start_position 32 | self.end_position = end_position 33 | 34 | def get_svg(self) -> svgwrite.base.BaseElement: 35 | """Convert the Fret to a svgwrite object. 36 | 37 | This maps the FretConfig configuration attributes to the svg 38 | attributes 39 | """ 40 | line = svgwrite.shapes.Line( 41 | start=self.start_position, 42 | end=self.end_position, 43 | stroke=self.config.color, 44 | stroke_width=self.config.width, 45 | ) 46 | return line 47 | -------------------------------------------------------------------------------- /fretboardgtr/elements/neck_dots.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional, Tuple 3 | 4 | import svgwrite 5 | 6 | from fretboardgtr.base import ConfigIniter 7 | from fretboardgtr.constants import BLACK, DARK_GRAY 8 | from fretboardgtr.elements.base import FretBoardElement 9 | 10 | 11 | @dataclass 12 | class NeckDotConfig(ConfigIniter): 13 | """NeckDot element configuration.""" 14 | 15 | color: str = DARK_GRAY 16 | stroke_color: str = BLACK 17 | stroke_width: int = 2 18 | radius: int = 7 19 | 20 | 21 | class NeckDot(FretBoardElement): 22 | """Neck dots elements to be drawn in the final fretboard.""" 23 | 24 | def __init__( 25 | self, position: Tuple[float, float], config: Optional[NeckDotConfig] = None 26 | ): 27 | self.config = config if config else NeckDotConfig() 28 | self.x = position[0] 29 | self.y = position[1] 30 | 31 | def get_svg(self) -> svgwrite.base.BaseElement: 32 | """Convert the NeckDot to a svgwrite object. 33 | 34 | This maps the NeckDotConfig configuration attributes to the svg 35 | attributes 36 | """ 37 | circle = svgwrite.shapes.Circle( 38 | (self.x, self.y), 39 | r=self.config.radius, 40 | fill=self.config.color, 41 | stroke=self.config.stroke_color, 42 | stroke_width=self.config.stroke_width, 43 | ) 44 | return circle 45 | -------------------------------------------------------------------------------- /fretboardgtr/elements/background.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional, Tuple 3 | 4 | import svgwrite 5 | 6 | from fretboardgtr.base import ConfigIniter 7 | from fretboardgtr.constants import NO_COLOR 8 | from fretboardgtr.elements.base import FretBoardElement 9 | 10 | 11 | @dataclass 12 | class BackgroundConfig(ConfigIniter): 13 | """Background element configuration.""" 14 | 15 | color: str = NO_COLOR 16 | opacity: float = 0.7 17 | 18 | 19 | class Background(FretBoardElement): 20 | """Background element to be drawn in the final fretboard.""" 21 | 22 | def __init__( 23 | self, 24 | position: Tuple[float, float], 25 | size: Tuple[float, float], 26 | config: Optional[BackgroundConfig] = None, 27 | ): 28 | self.config = config if config else BackgroundConfig() 29 | self.position = position 30 | self.size = size 31 | 32 | def get_svg(self) -> svgwrite.base.BaseElement: 33 | """Convert the Background to a svgwrite object. 34 | 35 | This maps the BackgroundConfig configuration attributes to the 36 | svg attributes 37 | """ 38 | rectangle = svgwrite.shapes.Rect( 39 | insert=self.position, 40 | size=self.size, # -2 evite case du bas du tuning 41 | rx=None, 42 | ry=None, 43 | fill=self.config.color, 44 | fill_opacity=self.config.opacity, 45 | ) 46 | return rectangle 47 | -------------------------------------------------------------------------------- /fretboardgtr/elements/cross.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional, Tuple 3 | 4 | from fretboardgtr.constants import BLACK 5 | 6 | TEXT_OFFSET = "0.3em" 7 | TEXT_STYLE = "text-anchor:middle" 8 | import svgwrite 9 | 10 | from fretboardgtr.base import ConfigIniter 11 | from fretboardgtr.elements.base import FretBoardElement 12 | 13 | 14 | @dataclass 15 | class CrossConfig(ConfigIniter): 16 | """Cross element configuration.""" 17 | 18 | color: str = BLACK 19 | fontsize: int = 35 20 | fontweight: str = "bold" 21 | 22 | 23 | class Cross(FretBoardElement): 24 | """Cross element to be drawn in the final fretboard.""" 25 | 26 | def __init__( 27 | self, 28 | position: Tuple[float, float], 29 | config: Optional[CrossConfig] = None, 30 | ): 31 | self.config = config if config else CrossConfig() 32 | self.name = "X" 33 | self.x = position[0] 34 | self.y = position[1] 35 | 36 | def get_svg(self) -> svgwrite.base.BaseElement: 37 | """Convert the Cross to a svgwrite object. 38 | 39 | This maps the CrossConfig configuration attributes to the svg 40 | attributes 41 | """ 42 | text = svgwrite.text.Text( 43 | self.name, 44 | insert=(self.x, self.y), 45 | dy=[TEXT_OFFSET], 46 | font_size=self.config.fontsize, 47 | fill=self.config.color, 48 | font_weight=self.config.fontweight, 49 | style=TEXT_STYLE, 50 | ) 51 | return text 52 | -------------------------------------------------------------------------------- /fretboardgtr/elements/strings.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional, Tuple 3 | 4 | import svgwrite 5 | 6 | from fretboardgtr.base import ConfigIniter 7 | from fretboardgtr.constants import BLACK 8 | from fretboardgtr.elements.base import FretBoardElement 9 | 10 | 11 | @dataclass 12 | class StringConfig(ConfigIniter): 13 | """String element configuration.""" 14 | 15 | color: str = BLACK 16 | width: int = 3 17 | 18 | 19 | class String(FretBoardElement): 20 | """String elements to be drawn in the final fretboard.""" 21 | 22 | def __init__( 23 | self, 24 | start_position: Tuple[float, float], 25 | end_position: Tuple[float, float], 26 | width: Optional[int] = None, 27 | config: Optional[StringConfig] = None, 28 | ): 29 | self.config = config if config else StringConfig() 30 | self.start_position = start_position 31 | self.end_position = end_position 32 | self.width = width 33 | 34 | def get_svg(self) -> svgwrite.base.BaseElement: 35 | """Convert the String to a svgwrite object. 36 | 37 | This maps the StringConfig configuration attributes to the svg 38 | attributes 39 | """ 40 | if self.width is None: 41 | self.width = self.config.width 42 | 43 | line = svgwrite.shapes.Line( 44 | start=self.start_position, 45 | end=self.end_position, 46 | stroke=self.config.color, 47 | stroke_width=self.width, 48 | ) 49 | return line 50 | -------------------------------------------------------------------------------- /fretboardgtr/elements/tuning.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional, Tuple 3 | 4 | import svgwrite 5 | 6 | from fretboardgtr.base import ConfigIniter 7 | from fretboardgtr.constants import GRAY 8 | from fretboardgtr.elements.base import FretBoardElement 9 | 10 | TEXT_OFFSET = "0.3em" 11 | TEXT_STYLE = "text-anchor:middle" 12 | 13 | 14 | @dataclass 15 | class TuningConfig(ConfigIniter): 16 | """Tuning element configuration.""" 17 | 18 | color: str = GRAY 19 | fontsize: int = 20 20 | fontweight: str = "normal" 21 | 22 | 23 | class Tuning(FretBoardElement): 24 | """Tuning texts elements to be drawn in the final fretboard.""" 25 | 26 | def __init__( 27 | self, 28 | name: str, 29 | position: Tuple[float, float], 30 | config: Optional[TuningConfig] = None, 31 | ): 32 | self.config = config if config else TuningConfig() 33 | self.name = name 34 | self.x = position[0] 35 | self.y = position[1] 36 | 37 | def get_svg(self) -> svgwrite.base.BaseElement: 38 | """Convert the Tuning to a svgwrite object. 39 | 40 | This maps the TuningConfig configuration attributes to the svg 41 | attributes 42 | """ 43 | text = svgwrite.text.Text( 44 | self.name, 45 | insert=(self.x, self.y), 46 | dy=[TEXT_OFFSET], 47 | fill=self.config.color, 48 | font_size=self.config.fontsize, 49 | font_weight=self.config.fontweight, 50 | style=TEXT_STYLE, 51 | ) 52 | return text 53 | -------------------------------------------------------------------------------- /fretboardgtr/elements/fret_number.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional, Tuple 3 | 4 | import svgwrite 5 | 6 | from fretboardgtr.base import ConfigIniter 7 | from fretboardgtr.constants import GRAY 8 | from fretboardgtr.elements.base import FretBoardElement 9 | 10 | TEXT_OFFSET = "0.3em" 11 | TEXT_STYLE = "text-anchor:middle" 12 | 13 | 14 | @dataclass 15 | class FretNumberConfig(ConfigIniter): 16 | """FretNumber element configuration.""" 17 | 18 | color: str = GRAY 19 | fontsize: int = 20 20 | fontweight: str = "bold" 21 | 22 | 23 | class FretNumber(FretBoardElement): 24 | """Fret numbers elements to be drawn in the final fretboard.""" 25 | 26 | def __init__( 27 | self, 28 | name: str, 29 | position: Tuple[float, float], 30 | config: Optional[FretNumberConfig] = None, 31 | ): 32 | self.config = config if config else FretNumberConfig() 33 | self.name = name 34 | self.x = position[0] 35 | self.y = position[1] 36 | 37 | def get_svg(self) -> svgwrite.base.BaseElement: 38 | """Convert the FretNumber to a svgwrite object. 39 | 40 | This maps the FretNumberConfig configuration attributes to the 41 | svg attributes 42 | """ 43 | text = svgwrite.text.Text( 44 | self.name, 45 | insert=(self.x, self.y), 46 | dy=[TEXT_OFFSET], 47 | fill=self.config.color, 48 | font_size=self.config.fontsize, 49 | font_weight=self.config.fontweight, 50 | style=TEXT_STYLE, 51 | ) 52 | return text 53 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/pull_request_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Pull request 3 | about: Please add description for a better understanding 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | # Description 10 | 11 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. 12 | 13 | Fixes # (issue) 14 | 15 | ## Type of change 16 | 17 | Please delete options that are not relevant. 18 | 19 | - [ ] Bug fix (non-breaking change which fixes an issue) 20 | - [ ] New feature (non-breaking change which adds functionality) 21 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 22 | - [ ] This change requires a documentation update 23 | 24 | # How Has This Been Tested? 25 | 26 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration 27 | 28 | - [ ] Test A 29 | - [ ] Test B 30 | 31 | **Test Configuration**: 32 | * Firmware version: 33 | * Hardware: 34 | * Toolchain: 35 | * SDK: 36 | 37 | # Checklist: 38 | 39 | - [ ] My code follows the style guidelines of this project 40 | - [ ] I have performed a self-review of my own code 41 | - [ ] I have commented my code, particularly in hard-to-understand areas 42 | - [ ] I have made corresponding changes to the documentation 43 | - [ ] My changes generate no new warnings 44 | - [ ] I have added tests that prove my fix is effective or that my feature works 45 | - [ ] New and existing unit tests pass locally with my changes 46 | - [ ] Any dependent changes have been merged and published in downstream modules 47 | -------------------------------------------------------------------------------- /fretboardgtr/fretboards/converters.py: -------------------------------------------------------------------------------- 1 | from dataclasses import fields 2 | 3 | import svgwrite 4 | 5 | from fretboardgtr.elements.base import FretBoardElement 6 | from fretboardgtr.fretboards.base import FretBoardLike 7 | 8 | 9 | class FretBoardToSVGConverter: 10 | """Convert a FretboardLike object to a svgwrite object. 11 | 12 | Convert it in order to export it to a specific format later. 13 | """ 14 | 15 | def __init__(self, fretboard: FretBoardLike): 16 | self._fretboard = fretboard 17 | self.drawing = self.get_empty() 18 | 19 | def get_empty(self) -> svgwrite.Drawing: 20 | """Create empty box and the object self.drawing.""" 21 | width, height = self._fretboard.get_size() 22 | return svgwrite.Drawing( 23 | size=(width, height), 24 | profile="full", 25 | ) 26 | 27 | def add_to_drawing( 28 | self, drawing: svgwrite.Drawing, element: FretBoardElement 29 | ) -> svgwrite.Drawing: 30 | if not issubclass(type(element), FretBoardElement): 31 | raise ValueError(f"Element {element} does not subclass FretBoardElement") 32 | drawing.add(element.get_svg()) 33 | return drawing 34 | 35 | def convert(self) -> svgwrite.Drawing: 36 | drawing = self.get_empty() 37 | elements = self._fretboard.get_elements() 38 | for key in fields(elements): 39 | element = getattr(elements, key.name, None) 40 | if element is None: 41 | continue 42 | if isinstance(element, list): 43 | for sub in element: 44 | drawing = self.add_to_drawing(drawing, sub) 45 | else: 46 | drawing = self.add_to_drawing(drawing, element) # type: ignore 47 | return drawing 48 | -------------------------------------------------------------------------------- /fretboardgtr/fretboards/base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import List, Optional, Tuple, Union 3 | 4 | from fretboardgtr.elements.base import FretBoardElement 5 | from fretboardgtr.elements.notes import FrettedNote, OpenNote 6 | from fretboardgtr.fretboards.config import FretBoardConfig 7 | from fretboardgtr.fretboards.elements import FretBoardElements 8 | from fretboardgtr.notes_creators import NotesContainer 9 | 10 | 11 | class FretBoardLike(ABC): 12 | """Interface to implement to define a FretBoard. 13 | 14 | This interface's purpose is to define an interface (common 15 | behaviour) in order to draw the fretboard into a SVG Format. This 16 | interface also define standard method for higher level API call such 17 | as add_note_element, or add_element for example. 18 | """ 19 | 20 | @abstractmethod 21 | def set_config(self, config: FretBoardConfig) -> None: 22 | pass 23 | 24 | @abstractmethod 25 | def add_note_element(self, note: Union[OpenNote, FrettedNote]) -> None: 26 | pass 27 | 28 | @abstractmethod 29 | def add_element(self, element: FretBoardElement) -> None: 30 | pass 31 | 32 | @abstractmethod 33 | def add_fingering( 34 | self, fingering: List[Optional[int]], root: Optional[str] = None 35 | ) -> None: 36 | pass 37 | 38 | @abstractmethod 39 | def add_notes(self, scale: NotesContainer) -> None: 40 | pass 41 | 42 | @abstractmethod 43 | def get_size(self) -> Tuple[float, float]: 44 | pass 45 | 46 | @abstractmethod 47 | def get_inside_bounds( 48 | self, 49 | ) -> Tuple[Tuple[float, float], Tuple[float, float]]: 50 | pass 51 | 52 | @abstractmethod 53 | def get_elements(self) -> FretBoardElements: 54 | pass 55 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.4.0 4 | hooks: 5 | - id: check-case-conflict 6 | - id: check-docstring-first 7 | - id: check-executables-have-shebangs 8 | - id: check-merge-conflict 9 | - id: check-shebang-scripts-are-executable 10 | - id: end-of-file-fixer 11 | exclude: .*.svg 12 | - id: fix-byte-order-marker 13 | - id: mixed-line-ending 14 | args: ["--fix", "lf"] 15 | - id: trailing-whitespace 16 | - id: check-json 17 | - id: check-toml 18 | - id: check-yaml 19 | - id: detect-private-key 20 | - id: name-tests-test 21 | args: ["--pytest-test-first"] 22 | - id: requirements-txt-fixer 23 | - repo: https://github.com/PyCQA/docformatter 24 | rev: v1.5.1 25 | hooks: 26 | - id: docformatter 27 | args: ["--in-place", "--config", "./pyproject.toml"] 28 | - repo: https://github.com/PyCQA/autoflake 29 | rev: v2.0.1 30 | hooks: 31 | - id: autoflake 32 | args: 33 | [ 34 | "--in-place", 35 | "--remove-all-unused-imports", 36 | "--ignore-init-module-imports", 37 | ] 38 | - repo: https://github.com/PyCQA/isort 39 | rev: 5.12.0 40 | hooks: 41 | - id: isort 42 | args: ["--profile", "black"] 43 | - repo: https://github.com/psf/black 44 | rev: 23.1.0 45 | hooks: 46 | - id: black 47 | entry: black 48 | require_serial: true 49 | - repo: https://github.com/pycqa/pydocstyle 50 | rev: 6.3.0 51 | hooks: 52 | - id: pydocstyle 53 | additional_dependencies: [".[toml]"] 54 | args: ["--config", ".pydocstyle.ini"] 55 | - repo: https://github.com/pycqa/flake8 56 | rev: 6.0.0 57 | hooks: 58 | - id: flake8 59 | -------------------------------------------------------------------------------- /tests/test_exporters.py: -------------------------------------------------------------------------------- 1 | import mimetypes 2 | import tempfile 3 | from pathlib import Path 4 | 5 | import pytest 6 | import svgwrite 7 | from PIL import Image 8 | from pypdf import PdfReader 9 | 10 | from fretboardgtr.exporters import PDFExporter, PNGExporter, SVGExporter 11 | 12 | 13 | @pytest.fixture() 14 | def drawing(): 15 | dwg = svgwrite.Drawing( 16 | size=( # +2 == Last fret + tuning 17 | 100, 18 | 100, 19 | ), 20 | profile="full", 21 | ) 22 | circle = svgwrite.shapes.Circle( 23 | (50, 50), 24 | r=20, 25 | ) 26 | dwg.add(circle) 27 | return dwg 28 | 29 | 30 | def test_svg_exporter(drawing): 31 | svg_exporter = SVGExporter(drawing) 32 | with tempfile.TemporaryDirectory() as tmp_dir: 33 | outfile = Path(tmp_dir) / "tmp_file.svg" 34 | assert not outfile.exists() 35 | svg_exporter.export(outfile) 36 | assert outfile.exists() 37 | assert mimetypes.guess_type(outfile)[0] == "image/svg+xml" 38 | 39 | 40 | def test_png_exporter(drawing): 41 | png_exporter = PNGExporter(drawing) 42 | with tempfile.TemporaryDirectory() as tmp_dir: 43 | outfile = Path(tmp_dir) / "tmp_file.png" 44 | assert not outfile.exists() 45 | png_exporter.export(outfile) 46 | assert outfile.exists() 47 | assert mimetypes.guess_type(outfile)[0] == "image/png" 48 | with Image.open(outfile) as img: 49 | assert img.format == "PNG" 50 | 51 | 52 | def test_pdf_exporter(drawing): 53 | pdf_exporter = PDFExporter(drawing) 54 | with tempfile.TemporaryDirectory() as tmp_dir: 55 | outfile = Path(tmp_dir) / "tmp_file.pdf" 56 | assert not outfile.exists() 57 | pdf_exporter.export(outfile) 58 | assert outfile.exists() 59 | assert mimetypes.guess_type(outfile)[0] == "application/pdf" 60 | assert len(PdfReader(outfile).pages) == 1 61 | -------------------------------------------------------------------------------- /fretboardgtr/fretboards/elements.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field, fields 2 | from typing import List, Optional, Union 3 | 4 | from fretboardgtr.elements.background import Background 5 | from fretboardgtr.elements.base import FretBoardElement 6 | from fretboardgtr.elements.cross import Cross 7 | from fretboardgtr.elements.fret_number import FretNumber 8 | from fretboardgtr.elements.frets import Fret 9 | from fretboardgtr.elements.neck_dots import NeckDot 10 | from fretboardgtr.elements.notes import FrettedNote, OpenNote 11 | from fretboardgtr.elements.nut import Nut 12 | from fretboardgtr.elements.strings import String 13 | from fretboardgtr.elements.tuning import Tuning 14 | 15 | 16 | @dataclass 17 | class FretBoardElements: 18 | """Container dataclass for the different elements of fretboards.""" 19 | 20 | background: Optional[Background] = None 21 | fret_numbers: List[FretNumber] = field(default_factory=list) 22 | neck_dots: List[NeckDot] = field(default_factory=list) 23 | frets: List[Fret] = field(default_factory=list) 24 | nut: Optional[Nut] = None 25 | tuning: List[Tuning] = field(default_factory=list) 26 | strings: List[String] = field(default_factory=list) 27 | notes: List[Union[OpenNote, FrettedNote]] = field(default_factory=list) 28 | crosses: List[Cross] = field(default_factory=list) 29 | customs: List[FretBoardElement] = field(default_factory=list) 30 | 31 | def to_list(self) -> List[FretBoardElement]: 32 | """Convert the elements to a flat list of element.""" 33 | flat_elements = [] 34 | for element in fields(self): 35 | value = getattr(self, element.name) 36 | if isinstance(value, list): 37 | flat_elements.extend(value) 38 | elif value is not None: 39 | flat_elements.append(value) 40 | return flat_elements 41 | 42 | def __len__(self) -> int: 43 | return len(self.to_list()) 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Project specific 3 | documentation 4 | 5 | # Coverage data 6 | coverage 7 | 8 | # tests outputs 9 | tests/data/outputs/* 10 | !tests/data/outputs/.placeholder 11 | 12 | # Byte-compiled / optimized / DLL files 13 | __pycache__/ 14 | *.py[cod] 15 | *$py.class 16 | 17 | # C extensions 18 | *.so 19 | 20 | # Distribution / packaging 21 | .Python 22 | env/ 23 | build/ 24 | develop-eggs/ 25 | dist/ 26 | downloads/ 27 | eggs/ 28 | .eggs/ 29 | lib/ 30 | lib64/ 31 | parts/ 32 | sdist/ 33 | var/ 34 | wheels/ 35 | *.egg-info/ 36 | .installed.cfg 37 | *.egg 38 | 39 | # PyInstaller 40 | # Usually these files are written by a python script from a template 41 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 42 | *.manifest 43 | *.spec 44 | 45 | # Installer logs 46 | pip-log.txt 47 | pip-delete-this-directory.txt 48 | 49 | # Unit test / coverage reports 50 | htmlcov/ 51 | .tox/ 52 | .coverage 53 | .coverage.* 54 | .cache 55 | nosetests.xml 56 | coverage.xml 57 | *.cover 58 | .hypothesis/ 59 | .pytest_cache/ 60 | 61 | # Translations 62 | *.mo 63 | *.pot 64 | 65 | # Django stuff: 66 | *.log 67 | local_settings.py 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | docs/_build/ 78 | 79 | # PyBuilder 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # celery beat schedule file 89 | celerybeat-schedule 90 | 91 | # SageMath parsed files 92 | *.sage.py 93 | 94 | # dotenv 95 | .env 96 | 97 | # virtualenv 98 | .venv 99 | venv/ 100 | ENV/ 101 | 102 | # Spyder project settings 103 | .spyderproject 104 | .spyproject 105 | 106 | # Rope project settings 107 | .ropeproject 108 | 109 | # mkdocs documentation 110 | /site 111 | 112 | # mypy 113 | .mypy_cache/ 114 | 115 | # IDE settings 116 | .vscode/ 117 | .idea/ 118 | -------------------------------------------------------------------------------- /docs/scripts/default.mplstyle: -------------------------------------------------------------------------------- 1 | date.autoformatter.year: %Y 2 | date.autoformatter.month: %Y-%m 3 | date.autoformatter.day: %Y-%m-%d 4 | date.autoformatter.hour: %m-%d %H 5 | date.autoformatter.minute: %d %H:%M 6 | date.autoformatter.second: %H:%M:%S 7 | date.autoformatter.microsecond: %M:%S.%f 8 | 9 | 10 | lines.linewidth: 1.5 # line width in points 11 | lines.linestyle: - # solid line 12 | lines.color: C0 # has no affect on plot(); see axes.prop_cycle 13 | lines.marker: o # the default marker 14 | lines.markerfacecolor: auto # the default marker face color 15 | lines.markeredgecolor: auto # the default marker edge color 16 | lines.markeredgewidth: 0 # the line width around the marker symbol 17 | lines.markersize: 4 # marker size, in points 18 | lines.solid_capstyle: round 19 | 20 | # Seaborn darkgrid parameters 21 | axes.grid: True 22 | axes.facecolor: F7F7F8 23 | axes.edgecolor: black 24 | axes.linewidth: 0.5 25 | axes.axisbelow: True 26 | axes.titlesize: 14 # font size of the axes title 27 | axes.titleweight: 600 # font weight of title 28 | axes.titlecolor: 636363 # color of the axes title, auto falls back to 29 | axes.labelsize: 12 # font size of the x and y labels 30 | axes.labelweight: 600 31 | axes.labelcolor: 636363 # font size of the x and y labels 32 | 33 | 34 | xtick.minor.visible: True # visibility of minor ticks on x-axis 35 | ytick.minor.visible: True # visibility of minor ticks on y-axis 36 | xtick.major.size: 5 37 | ytick.major.size: 5 38 | xtick.minor.size: 2 39 | ytick.minor.size: 2 40 | xtick.color: 636363 41 | ytick.color: 636363 42 | xtick.direction: out 43 | ytick.direction: out 44 | 45 | grid.color: F0F0F0 46 | grid.linestyle: -- # solid 47 | grid.linewidth: 2 # in points 48 | 49 | # Custom 50 | font.family: sans-serif 51 | font.style: italic 52 | 53 | # XXXXXX(XX) Add two last chars to represent the transparency : 0 to 255 in hex format. 200=C8 54 | axes.prop_cycle: cycler('color', ['4F81BD', 'E46C0A', 'C0504D', '9BBB59', '8064A2']) 55 | 56 | image.cmap: RdPu 57 | figure.facecolor: fefeff 58 | savefig.facecolor: fefeff 59 | 60 | legend.frameon: True # if True, draw the legend on a background patch 61 | legend.framealpha: 0.8 62 | legend.facecolor: white 63 | legend.numpoints: 1 64 | legend.scatterpoints: 1 65 | legend.fontsize: 12 66 | -------------------------------------------------------------------------------- /fretboardgtr/note_colors.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from fretboardgtr.constants import INTERVAL_MAPPING, WHITE 4 | 5 | TEXT_OFFSET = "0.3em" 6 | TEXT_STYLE = "text-anchor:middle" 7 | from fretboardgtr.base import ConfigIniter 8 | 9 | 10 | @dataclass 11 | class NoteColors(ConfigIniter): 12 | """Dataclass containing the mapping of colors and intervals.""" 13 | 14 | root: str = "rgb(231, 0, 0)" 15 | minor_second: str = "rgb(249, 229, 0)" 16 | major_second: str = "rgb(249, 165, 0)" 17 | minor_third: str = "rgb(0, 94, 0)" 18 | major_third: str = "rgb(0, 108, 0)" 19 | perfect_fourth: str = "rgb(0, 154, 0)" 20 | diminished_fifth: str = "rgb(0, 15, 65)" 21 | perfect_fifth: str = "rgb(0, 73, 151)" 22 | minor_sixth: str = "rgb(168, 107, 98)" 23 | major_sixth: str = "rgb(222, 81, 108)" 24 | minor_seventh: str = "rgb(120, 37, 134)" 25 | major_seventh: str = "rgb(120, 25, 98)" 26 | 27 | def from_short_interval(self, interval: str) -> str: 28 | """Get color for the given short interval. 29 | 30 | Parameters 31 | ---------- 32 | interval : str 33 | String representing the interval 34 | 35 | Returns 36 | ------- 37 | str 38 | RGB color as string 39 | 40 | Example 41 | ------- 42 | from fretboardgtr.constants import Interval 43 | >>> NoteColors().from_short_interval(Interval.MINOR_SIXTH) 44 | "rgb(168, 107, 98)" 45 | """ 46 | color = WHITE 47 | for long, short in INTERVAL_MAPPING.items(): 48 | if interval != short: 49 | continue 50 | if hasattr(self, long): 51 | color = getattr(self, long) 52 | return color 53 | 54 | def from_interval(self, interval: int) -> str: 55 | """Get color for the given long interval name. 56 | 57 | Parameters 58 | ---------- 59 | interval : str 60 | String representing the long interval 61 | 62 | Returns 63 | ------- 64 | str 65 | RGB color as string 66 | 67 | Example 68 | ------- 69 | from fretboardgtr.constants import LongInterval 70 | >>> NoteColors().from_short_interval(LongInterval.MINOR_SIXTH) 71 | "rgb(168, 107, 98)" 72 | """ 73 | cls_keys = list(self.__annotations__) 74 | color = getattr(self, cls_keys[interval % 12]) 75 | return color 76 | -------------------------------------------------------------------------------- /tests/elements/test_notes.py: -------------------------------------------------------------------------------- 1 | from fretboardgtr.elements.notes import ( 2 | FrettedNote, 3 | FrettedNoteConfig, 4 | OpenNote, 5 | OpenNoteConfig, 6 | ) 7 | 8 | 9 | def test_open_note_get_svg(): 10 | open_note = OpenNote(name="test", position=(0.0, 0.0)) 11 | circle = open_note.get_svg().elements[0] 12 | text = open_note.get_svg().elements[1] 13 | circle_attribs = circle.attribs 14 | assert circle_attribs["cx"] == 0.0 15 | assert circle_attribs["cy"] == 0.0 16 | 17 | assert text.text == "test" 18 | text_attribs = text.attribs 19 | assert float(text_attribs["x"]) == 0.0 20 | assert float(text_attribs["y"]) == 0.0 21 | 22 | 23 | def test_open_note_get_svg_custom_config(): 24 | open_note_config = OpenNoteConfig(radius=30) 25 | open_note = OpenNote(name="test", position=(0.0, 0.0), config=open_note_config) 26 | circle = open_note.get_svg().elements[0] 27 | text = open_note.get_svg().elements[1] 28 | circle_attribs = circle.attribs 29 | assert circle_attribs["cx"] == 0.0 30 | assert circle_attribs["cy"] == 0.0 31 | assert circle_attribs["r"] == 30 32 | 33 | assert text.text == "test" 34 | text_attribs = text.attribs 35 | assert float(text_attribs["x"]) == 0.0 36 | assert float(text_attribs["y"]) == 0.0 37 | 38 | 39 | def test_fretted_note_get_svg(): 40 | fretted_note = FrettedNote(name="test", position=(0.0, 0.0)) 41 | circle = fretted_note.get_svg().elements[0] 42 | text = fretted_note.get_svg().elements[1] 43 | circle_attribs = circle.attribs 44 | assert circle_attribs["cx"] == 0.0 45 | assert circle_attribs["cy"] == 0.0 46 | 47 | assert text.text == "test" 48 | text_attribs = text.attribs 49 | assert float(text_attribs["x"]) == 0.0 50 | assert float(text_attribs["y"]) == 0.0 51 | 52 | 53 | def test_fretted_note_get_svg_custom_config(): 54 | fretted_note_config = FrettedNoteConfig(radius=30) 55 | fretted_note = FrettedNote( 56 | name="test", position=(0.0, 0.0), config=fretted_note_config 57 | ) 58 | circle = fretted_note.get_svg().elements[0] 59 | text = fretted_note.get_svg().elements[1] 60 | circle_attribs = circle.attribs 61 | assert circle_attribs["cx"] == 0.0 62 | assert circle_attribs["cy"] == 0.0 63 | assert circle_attribs["r"] == 30 64 | 65 | assert text.text == "test" 66 | text_attribs = text.attribs 67 | assert float(text_attribs["x"]) == 0.0 68 | assert float(text_attribs["y"]) == 0.0 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FretBoardGtr 2 | 3 | Package that make easy creation of **highly customizable** fretboards and chords diagrams 4 | 5 |

6 | CI Status 7 | Documentation Status 8 | PyPI 9 | Code style: black 10 | Coverage Status 11 | License: GNU Affero General Public License v3.0 12 | Issue Badge 13 | Pull requests Badge 14 |

15 | 16 | - License: GNU Affero General Public License v3.0 17 | - Documentation: https://fretboardgtr.readthedocs.io/en/latest. 18 | 19 | # Get started 20 | 21 | To get started simply install the package from PyPI 22 | 23 | ## Dependencies 24 | 25 | `fretboardgtr` needs to have the following install in order to run : 26 | 27 | ```shell 28 | sudo apt install libcairo2-dev pkg-config 29 | ``` 30 | 31 | ## How to install 32 | 33 | 34 | 35 | ```shell 36 | pip install fretboardgtr 37 | ``` 38 | 39 | ## Usage 40 | 41 | ```python 42 | from fretboardgtr.fretboard import FretBoard 43 | from fretboardgtr.notes_creators import ScaleFromName 44 | 45 | fretboard = FretBoard() 46 | c_major = ScaleFromName(root="C", mode="Ionian").build() 47 | fretboard.add_notes(scale=c_major) 48 | fretboard.export("my_fretboard.svg", format="svg") 49 | ``` 50 | 51 | ![My Fretboard](docs/source/assets/my_fretboard.svg) 52 | 53 | ## Documentation 54 | 55 | All the documentation can be found in the [documentation](https://fretboardgtr.readthedocs.io/en/latest) 56 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | 2 | 3 | [build-system] 4 | requires = ["setuptools"] 5 | build-backend = "setuptools.build_meta" 6 | 7 | [project] 8 | name = "fretboardgtr" 9 | authors = [ 10 | { name="Antoine Gibek", email="antoine.gibek@gmail.com"}, 11 | ] 12 | description="Package that make easy creation of fretboards and chords diagrams" 13 | readme = "README.md" 14 | requires-python = ">=3.8" 15 | classifiers = [ 16 | 'Development Status :: 2 - Pre-Alpha', 17 | 'Intended Audience :: Developers', 18 | 'License :: OSI Approved :: GNU Affero General Public License v3', 19 | 'Natural Language :: English', 20 | 'Programming Language :: Python :: Implementation :: CPython', 21 | 'Programming Language :: Python :: 3', 22 | 'Programming Language :: Python :: 3.8', 23 | 'Programming Language :: Python :: 3.9', 24 | 'Programming Language :: Python :: 3.10', 25 | 'Programming Language :: Python :: 3.11', 26 | ] 27 | keywords=['fretboardgtr', "fretboard", "chord", "guitar", "bass"] 28 | dependencies=["reportlab<=4", "svglib", "svgwrite", "pypdf"] 29 | # Dynamic for setuptools 30 | dynamic = ["version"] 31 | 32 | [project.license] 33 | file = "LICENSE" 34 | 35 | [project.urls] 36 | homepage = "https://github.com/antscloud/fretboardgtr" 37 | documentation = "https://fretboardgtr.readthedocs.io/en/latest" 38 | repository = "https://github.com/antscloud/fretboardgtr" 39 | changelog = "https://github.com/antscloud/fretboardgtr/blob/main/CHANGELOG.md" 40 | "Bug Tracker" = "https://github.com/antscloud/fretboardgtr/issues" 41 | 42 | [project.optional-dependencies] 43 | dev = [ 44 | "fretboardgtr", 45 | # Pytest 46 | "pytest>=6.1.1", 47 | "pytest-cov>=2.10.1", 48 | "pytest-mock>=3.6.1", 49 | "coverage>=5.3", 50 | 51 | # Documentation 52 | "sphinx>=4.5.0", 53 | "myst", 54 | "myst-parser", 55 | "sphinx_book_theme>=0.3.0", 56 | 57 | # Pre-commit 58 | "pre-commit", 59 | 60 | # Formatting 61 | "black", 62 | "flake8", 63 | 64 | # Typing 65 | "mypy", 66 | 67 | # Python version capabilities 68 | "six", 69 | 70 | # Utils 71 | "pypdf" 72 | ] 73 | 74 | [tool.setuptools] 75 | zip-safe=false 76 | 77 | [tool.setuptools.packages.find] 78 | include = ["fretboardgtr", "fretboardgtr.*"] 79 | 80 | [tool.mypy] 81 | explicit_package_bases = true 82 | disallow_untyped_defs = true 83 | ignore_missing_imports = true 84 | exclude = ['build/','venv', "tests", "docs"] 85 | 86 | [tool.docformatter] 87 | recursive = true 88 | blank = true 89 | syntax= "numpy" 90 | -------------------------------------------------------------------------------- /fretboardgtr/fretboards/config.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | 3 | from fretboardgtr.base import ConfigIniter 4 | from fretboardgtr.elements.background import BackgroundConfig 5 | from fretboardgtr.elements.cross import CrossConfig 6 | from fretboardgtr.elements.fret_number import FretNumberConfig 7 | from fretboardgtr.elements.frets import FretConfig 8 | from fretboardgtr.elements.neck_dots import NeckDotConfig 9 | from fretboardgtr.elements.notes import FrettedNoteConfig, OpenNoteConfig 10 | from fretboardgtr.elements.nut import NutConfig 11 | from fretboardgtr.elements.strings import StringConfig 12 | from fretboardgtr.elements.tuning import TuningConfig 13 | from fretboardgtr.note_colors import NoteColors 14 | 15 | 16 | @dataclass 17 | class FretBoardGeneralConfig(ConfigIniter): 18 | """General configuration for a Fretboard.""" 19 | 20 | x_start: float = 30.0 21 | y_start: float = 30.0 22 | x_end_offset: float = 0.0 23 | y_end_offset: float = 0.0 24 | fret_height: int = 50 25 | fret_width: int = 70 26 | first_fret: int = 1 27 | last_fret: int = 12 28 | show_tuning: bool = True 29 | show_frets: bool = True 30 | show_nut: bool = True 31 | show_degree_name: bool = False 32 | show_note_name: bool = True 33 | open_color_scale: bool = False 34 | fretted_color_scale: bool = True 35 | open_colors: NoteColors = field(default_factory=NoteColors) 36 | fretted_colors: NoteColors = field(default_factory=NoteColors) 37 | enharmonic: bool = True 38 | 39 | 40 | @dataclass 41 | class FretBoardConfig(ConfigIniter): 42 | """Configuration for the fretboard but also for all its elements. 43 | 44 | Inside configurations are : 45 | 46 | FretBoardGeneralConfig 47 | BackgroundConfig 48 | FretNumberConfig 49 | NeckDotConfig 50 | FretConfig 51 | NutConfig 52 | TuningConfig 53 | StringConfig 54 | OpenNoteConfig 55 | FrettedNoteConfig 56 | CrossConfig 57 | """ 58 | 59 | general: FretBoardGeneralConfig = field(default_factory=FretBoardGeneralConfig) 60 | background: BackgroundConfig = field(default_factory=BackgroundConfig) 61 | fret_numbers: FretNumberConfig = field(default_factory=FretNumberConfig) 62 | neck_dots: NeckDotConfig = field(default_factory=NeckDotConfig) 63 | frets: FretConfig = field(default_factory=FretConfig) 64 | nut: NutConfig = field(default_factory=NutConfig) 65 | tuning: TuningConfig = field(default_factory=TuningConfig) 66 | strings: StringConfig = field(default_factory=StringConfig) 67 | open_notes: OpenNoteConfig = field(default_factory=OpenNoteConfig) 68 | fretted_notes: FrettedNoteConfig = field(default_factory=FrettedNoteConfig) 69 | cross: CrossConfig = field(default_factory=CrossConfig) 70 | 71 | def validate(self) -> "FretBoardConfig": 72 | if self.general.first_fret <= 0: 73 | self.general.first_fret = 1 74 | if self.general.last_fret < self.general.first_fret: 75 | self.general.last_fret = self.general.first_fret 76 | return self 77 | -------------------------------------------------------------------------------- /fretboardgtr/exporters.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | import uuid 3 | from abc import ABC, abstractmethod 4 | from pathlib import Path 5 | from typing import Dict, Type, Union 6 | 7 | import svgwrite 8 | 9 | 10 | class Exporter(ABC): 11 | """Interface to implement for a new exporter.""" 12 | 13 | def __init__(self, drawing: svgwrite.Drawing): 14 | self.drawing = drawing 15 | 16 | @abstractmethod 17 | def export(self, to: Union[str, Path]) -> None: 18 | pass 19 | 20 | 21 | class SVGExporter(Exporter): 22 | """SVG Exporter.""" 23 | 24 | def export(self, to: Union[str, Path]) -> None: 25 | to = Path(to) 26 | self.drawing.saveas(str(to)) 27 | 28 | 29 | class PNGExporter(Exporter): 30 | """PNG Exporter. 31 | 32 | Need reportlab and svglib module installed 33 | """ 34 | 35 | def export(self, to: Union[str, Path]) -> None: 36 | try: 37 | from reportlab.graphics import renderPM 38 | except ImportError: 39 | raise ImportError( 40 | "Cannot export svg to PNG because reportlab package is missing" 41 | ) 42 | try: 43 | from svglib.svglib import svg2rlg 44 | except ImportError: 45 | raise ImportError( 46 | "Cannot export svg to PNG because svglib package is missing" 47 | ) 48 | to = Path(to) 49 | with tempfile.TemporaryDirectory() as tmp_dir: 50 | tmp_file = Path(tmp_dir) / Path(f"{uuid.uuid4()}.svg") 51 | self.drawing.saveas(str(tmp_file)) 52 | drawing = svg2rlg(str(tmp_file)) 53 | renderPM.drawToFile(drawing, str(to), fmt="PNG") 54 | 55 | 56 | class PDFExporter(Exporter): 57 | """PDF Exporter. 58 | 59 | Need reportlab and svglib module installed 60 | """ 61 | 62 | def export(self, to: Union[str, Path]) -> None: 63 | try: 64 | from reportlab.graphics import renderPDF 65 | except ImportError: 66 | raise ImportError( 67 | "Cannot export svg to PNG because reportlab package is missing" 68 | ) 69 | try: 70 | from svglib.svglib import svg2rlg 71 | except ImportError: 72 | raise ImportError( 73 | "Cannot export svg to PNG because svglib package is missing" 74 | ) 75 | to = Path(to) 76 | with tempfile.TemporaryDirectory() as tmp_dir: 77 | tmp_file = Path(tmp_dir) / Path(f"{uuid.uuid4()}.svg") 78 | self.drawing.saveas(str(tmp_file)) 79 | 80 | drawing = svg2rlg(str(tmp_file)) 81 | renderPDF.drawToFile(drawing, str(to)) 82 | 83 | 84 | def register_exporter(exporter: Type[Exporter], extension: str) -> None: 85 | """Register an exporter. 86 | 87 | When creating a new exporter one have to use this function to 88 | register the exporter and be able to use it 89 | """ 90 | EXPORTERS[extension.upper()] = exporter 91 | 92 | 93 | EXPORTERS: Dict[str, Type[Exporter]] = {} 94 | 95 | register_exporter(SVGExporter, "SVG") 96 | register_exporter(PNGExporter, "PNG") 97 | register_exporter(PDFExporter, "PDF") 98 | -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | # git-cliff ~ default configuration file 2 | # https://git-cliff.org/docs/configuration 3 | # 4 | # Lines starting with "#" are comments. 5 | # Configuration options are organized into tables and keys. 6 | # See documentation for more information on available options. 7 | 8 | [changelog] 9 | # changelog header 10 | header = """ 11 | # Changelog\n 12 | All notable changes to this project will be documented in this file.\n 13 | """ 14 | # template for the changelog body 15 | # https://keats.github.io/tera/docs/#introduction 16 | body = """ 17 | {% if version %}\ 18 | ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} 19 | {% else %}\ 20 | ## [unreleased] 21 | {% endif %}\ 22 | {% for group, commits in commits | group_by(attribute="group") %} 23 | ### {{ group | upper_first }} 24 | {% for commit in commits %} 25 | - {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\ 26 | {% endfor %} 27 | {% endfor %}\n 28 | """ 29 | # remove the leading and trailing whitespace from the template 30 | trim = true 31 | # changelog footer 32 | footer = """ 33 | 34 | """ 35 | # postprocessors 36 | postprocessors = [ 37 | # { pattern = '', replace = "https://github.com/orhun/git-cliff" }, # replace repository URL 38 | ] 39 | [git] 40 | # parse the commits based on https://www.conventionalcommits.org 41 | conventional_commits = true 42 | # filter out the commits that are not conventional 43 | filter_unconventional = true 44 | # process each line of a commit as an individual commit 45 | split_commits = false 46 | # regex for preprocessing the commit messages 47 | commit_preprocessors = [ 48 | # { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](/issues/${2}))"}, # replace issue numbers 49 | ] 50 | # regex for parsing and grouping commits 51 | commit_parsers = [ 52 | { message = "^feat", group = "Features" }, 53 | { message = "^fix", group = "Bug Fixes" }, 54 | { message = "^doc", group = "Documentation" }, 55 | { message = "^perf", group = "Performance" }, 56 | { message = "^refactor", group = "Refactor" }, 57 | { message = "^style", group = "Styling" }, 58 | { message = "^test", group = "Testing" }, 59 | { message = "^chore\\(release\\): prepare for", skip = true }, 60 | { message = "^chore\\(deps\\)", skip = true }, 61 | { message = "^chore\\(pr\\)", skip = true }, 62 | { message = "^chore\\(pull\\)", skip = true }, 63 | { message = "^chore|ci", group = "Miscellaneous Tasks" }, 64 | { body = ".*security", group = "Security" }, 65 | { message = "^revert", group = "Revert" }, 66 | ] 67 | # protect breaking changes from being skipped due to matching a skipping commit_parser 68 | protect_breaking_commits = false 69 | # filter out the commits that are not matched by commit parsers 70 | filter_commits = false 71 | # regex for matching git tags 72 | tag_pattern = "(v)?[0-9].*" 73 | 74 | # regex for skipping tags 75 | skip_tags = "v0.1.0-beta.1" 76 | # regex for ignoring tags 77 | ignore_tags = "" 78 | # sort the tags topologically 79 | topo_order = false 80 | # sort the commits inside sections by oldest/newest order 81 | sort_commits = "oldest" 82 | # limit the number of commits included in the changelog. 83 | # limit_commits = 42 84 | -------------------------------------------------------------------------------- /tests/test_notes.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from fretboardgtr.notes import Note 4 | 5 | 6 | @pytest.mark.parametrize("invalid_note", ["Z", "H", "Invalid"]) 7 | def test_invalid_note_creation(invalid_note): 8 | with pytest.raises(ValueError): 9 | Note(invalid_note) 10 | 11 | 12 | @pytest.mark.parametrize( 13 | "valid_note", 14 | ["C", "C#", "C###" "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"], 15 | ) 16 | def test_valid_note_creation(valid_note): 17 | note = Note(valid_note) 18 | assert str(note) == valid_note 19 | 20 | 21 | @pytest.mark.parametrize( 22 | "note_str, expected_resolved_note, prefer_flat", 23 | [ 24 | ("Fb", "E", False), 25 | ("D#", "D#", False), 26 | ("Ab", "Ab", True), 27 | ("Abbb", "Gb", True), 28 | ("F#", "F#", False), 29 | ("E#", "F", False), 30 | ("G#", "G#", False), 31 | ("A#", "A#", False), 32 | ("B#", "C", False), 33 | ], 34 | ) 35 | def test_resolve(note_str, expected_resolved_note, prefer_flat): 36 | note = Note(note_str) 37 | resolved_note = note.resolve(prefer_flat) 38 | assert str(resolved_note) == expected_resolved_note 39 | 40 | 41 | @pytest.mark.parametrize( 42 | "note_str, expected_sharpened_note", 43 | [ 44 | ("D", "D#"), 45 | ("C", "C#"), 46 | ("Db", "D"), 47 | ("Dbb", "Db"), 48 | ("Dbbbb", "Dbbb"), 49 | ("D#", "D##"), 50 | ], 51 | ) 52 | def test_sharpen(note_str, expected_sharpened_note): 53 | note = Note(note_str) 54 | sharpened_note = note.sharpen() 55 | assert str(sharpened_note) == expected_sharpened_note 56 | 57 | 58 | @pytest.mark.parametrize( 59 | "note_str, expected_flattened_note", 60 | [ 61 | ("D", "Db"), 62 | ("C", "Cb"), 63 | ("Db", "Dbb"), 64 | ("D#", "D"), 65 | ], 66 | ) 67 | def test_flatten(note_str, expected_flattened_note): 68 | note = Note(note_str) 69 | flattened_note = note.flatten() 70 | assert str(flattened_note) == expected_flattened_note 71 | 72 | 73 | @pytest.mark.parametrize( 74 | "note_str, expected_flat_enharmonic_note", 75 | [ 76 | ("A#", "Bb"), 77 | ("A##", "B"), 78 | ("A###", "C"), 79 | ("A####", "Db"), 80 | ("A", "Bbb"), 81 | ("Ab", "Bbbb"), 82 | ("G#", "Ab"), 83 | ("G", "Abb"), 84 | ("Gb", "Abbb"), 85 | ("F#", "Gb"), 86 | ("F", "Gbb"), 87 | ("E", "Fb"), 88 | ("E#", "F"), 89 | ("Eb", "Fbb"), 90 | ], 91 | ) 92 | def test_flat_enharmonic(note_str, expected_flat_enharmonic_note): 93 | note = Note(note_str) 94 | flat_enharmonic_note = note.flat_enharmonic() 95 | assert str(flat_enharmonic_note) == expected_flat_enharmonic_note 96 | 97 | 98 | @pytest.mark.parametrize( 99 | "note_str, expected_sharp_enharmonic_note", 100 | [ 101 | ("Ab", "G#"), 102 | ("Abb", "G"), 103 | ("Abbb", "F#"), 104 | ("Abbbb", "F"), 105 | ("A", "G##"), 106 | ("A#", "G###"), 107 | ("Gb", "F#"), 108 | ("G", "F##"), 109 | ("G#", "F###"), 110 | ("Fb", "E"), 111 | ("F", "E#"), 112 | ("E", "D##"), 113 | ("Eb", "D#"), 114 | ], 115 | ) 116 | def test_sharp_enharmonic(note_str, expected_sharp_enharmonic_note): 117 | note = Note(note_str) 118 | sharp_enharmonic_note = note.sharp_enharmonic() 119 | assert str(sharp_enharmonic_note) == expected_sharp_enharmonic_note 120 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [0.2.7] - 2024-01-23 6 | 7 | ### Bug Fixes 8 | 9 | - Scale_to_enharmonic function and make it more logical 10 | 11 | ### Documentation 12 | 13 | - Auto-generated changelog 14 | - Update contributing for changelogs 15 | - Auto-generated changelog 16 | 17 | ## [0.2.6] - 2024-01-23 18 | 19 | ### Features 20 | 21 | - Update docs with figure 22 | - Add note class to handle sharps, flats and stuffs 23 | - Add better docstring 24 | 25 | ### Refactor 26 | 27 | - Make creation of notes clearer and add option to continue all over the board the fretboard notes for add_scale 28 | - Change enums names 29 | 30 | ## [0.2.5] - 2023-12-20 31 | 32 | ### Features 33 | 34 | - Add some utils functions to handle scales 35 | - Make first fret 0 indexed to make more coherent 36 | 37 | ## [0.2.4] - 2023-12-19 38 | 39 | ### Features 40 | 41 | - Add enharmonic support and fix typo/ 42 | 43 | ## [0.2.3] - 2023-11-15 44 | 45 | ### Features 46 | 47 | - Add scale positions generator 48 | 49 | ## [0.2.2] - 2023-11-13 50 | 51 | ### Bug Fixes 52 | 53 | - PNGExporter now exports to PNG instead of GIF 54 | - Context manager failed for windows 55 | 56 | ## [0.2.1] - 2023-10-13 57 | 58 | ### Bug Fixes 59 | 60 | - Docs compilation and add dependency 61 | - Apply pre-commit on all files 62 | 63 | ### Features 64 | 65 | - Accept config as a dict 66 | - Remove bump2version as it is not trivial 67 | 68 | ### Miscellaneous Tasks 69 | 70 | - Re-add codecov token 71 | 72 | ## [0.2.0] - 2023-07-28 73 | 74 | ### Bug Fixes 75 | 76 | - Fixes #20 77 | - The keyword 'main' in the docs was 'general' 78 | - Starting at a fret != 0 was not working 79 | 80 | ### Documentation 81 | 82 | - Fixes new custom fretboard from example 83 | - Add examples 84 | 85 | ### Features 86 | 87 | - Simplify implementation of both vertical and horizontal fretboard 88 | - Add generation of fingering 89 | 90 | ## [0.1.1-dev0] - 2023-02-17 91 | 92 | ### Bug Fixes 93 | 94 | - Make a pre-release cycle and documentation of contributing 95 | 96 | ## [0.1.0-dev1] - 2023-02-17 97 | 98 | ### Bug Fixes 99 | 100 | - Updates badges default branch 101 | - Remove release_candidate as it is tricky to do 102 | 103 | ## [0.1.0-rc1] - 2023-02-17 104 | 105 | ### Bug Fixes 106 | 107 | - FIxes last hooks failing 108 | - Add new default for background 109 | - Solve imports and e2e 110 | - E2e tests were not consistent, just check existence and add to artifacts 111 | - Two times the same test 112 | 113 | ### Documentation 114 | 115 | - Add sphinx documentation 116 | - Rewrite docs from refactoring 117 | - Add vertical get started 118 | - Add docstrings in almost all new contents 119 | 120 | ### Features 121 | 122 | - Create a more SOLID approach for exports 123 | - Assemble all the component for the fretboard in a more SOLIDISH way 124 | - Add highly recommended file 125 | - Add full packaging and CI/CD project setup 126 | - Apply all pre-commit hooks 127 | - Add fingering on fretboard possible/ add cross element 128 | - Add vertical fretboard and assemble parts 129 | 130 | ### Miscellaneous Tasks 131 | 132 | - Clean up useless constants 133 | - Clean repo 134 | - Fix bug from isort version, switch to 5.11.5 135 | 136 | ### Refactor 137 | 138 | - Extract all the component of fretboard into a collection of elements 139 | - Add utils functions common for chord and fretboard 140 | - Extract creation of scale and chord 141 | - Add all tests and organize class 142 | - Separation of concern 143 | - Split logic into more atomic files 144 | 145 | 146 | -------------------------------------------------------------------------------- /fretboardgtr/elements/notes.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional, Tuple 3 | 4 | from fretboardgtr.constants import BLACK, WHITE 5 | 6 | TEXT_OFFSET = "0.3em" 7 | TEXT_STYLE = "text-anchor:middle" 8 | import svgwrite 9 | 10 | from fretboardgtr.base import ConfigIniter 11 | from fretboardgtr.elements.base import FretBoardElement 12 | 13 | 14 | @dataclass 15 | class OpenNoteConfig(ConfigIniter): 16 | """OpenNote element configuration.""" 17 | 18 | radius: int = 20 19 | color: str = WHITE 20 | stroke_color: str = BLACK 21 | stroke_width: int = 3 22 | text_color: str = BLACK 23 | fontsize: int = 20 24 | fontweight: str = "bold" 25 | 26 | 27 | class OpenNote(FretBoardElement): 28 | """Open notes elements to be drawn in the final fretboard.""" 29 | 30 | def __init__( 31 | self, 32 | name: str, 33 | position: Tuple[float, float], 34 | config: Optional[OpenNoteConfig] = None, 35 | ): 36 | self.config = config if config else OpenNoteConfig() 37 | self.name = name 38 | self.x = position[0] 39 | self.y = position[1] 40 | 41 | def get_svg(self) -> svgwrite.base.BaseElement: 42 | """Convert the OpenNote to a svgwrite object. 43 | 44 | This maps the OpenNoteConfig configuration attributes to the svg 45 | attributes 46 | """ 47 | note = svgwrite.container.Group() 48 | circle = svgwrite.shapes.Circle( 49 | (self.x, self.y), 50 | r=self.config.radius, 51 | fill=self.config.color, 52 | stroke=self.config.stroke_color, 53 | stroke_width=self.config.stroke_width, 54 | ) 55 | text = svgwrite.text.Text( 56 | self.name, 57 | insert=(self.x, self.y), 58 | dy=[TEXT_OFFSET], 59 | font_size=self.config.fontsize, 60 | fill=self.config.text_color, 61 | font_weight=self.config.fontweight, 62 | style=TEXT_STYLE, 63 | ) 64 | note.add(circle) 65 | note.add(text) 66 | return note 67 | 68 | 69 | @dataclass 70 | class FrettedNoteConfig(ConfigIniter): 71 | radius: int = 20 72 | color: str = WHITE 73 | stroke_color: str = BLACK 74 | stroke_width: int = 3 75 | text_color: str = BLACK 76 | fontsize: int = 20 77 | fontweight: str = "bold" 78 | 79 | 80 | class FrettedNote(FretBoardElement): 81 | """Fretted notes elements to be drawn in the final fretboard.""" 82 | 83 | def __init__( 84 | self, 85 | name: str, 86 | position: Tuple[float, float], 87 | config: Optional[FrettedNoteConfig] = None, 88 | ): 89 | self.config = config if config else FrettedNoteConfig() 90 | self.name = name 91 | self.x = position[0] 92 | self.y = position[1] 93 | 94 | def get_svg(self) -> svgwrite.base.BaseElement: 95 | """Convert the FrettedNote to a svgwrite object. 96 | 97 | This maps the FrettedNoteConfig configuration attributes to the 98 | svg attributes 99 | """ 100 | note = svgwrite.container.Group() 101 | circle = svgwrite.shapes.Circle( 102 | (self.x, self.y), 103 | r=self.config.radius, 104 | fill=self.config.color, 105 | stroke=self.config.stroke_color, 106 | stroke_width=self.config.stroke_width, 107 | ) 108 | 109 | text = svgwrite.text.Text( 110 | self.name, 111 | insert=(self.x, self.y), 112 | dy=[TEXT_OFFSET], 113 | font_size=self.config.fontsize, 114 | fill=self.config.text_color, 115 | font_weight="bold", 116 | style=TEXT_STYLE, 117 | ) 118 | note.add(circle) 119 | note.add(text) 120 | return note 121 | -------------------------------------------------------------------------------- /docs/source/assets/c_major_chord.svg: -------------------------------------------------------------------------------- 1 | 2 | 35EADGBEECEGCE -------------------------------------------------------------------------------- /docs/source/assets/C_M/C_M_position_0.svg: -------------------------------------------------------------------------------- 1 | 2 | 35EADGBEECEGCE -------------------------------------------------------------------------------- /docs/source/assets/C_M/C_M_position_1.svg: -------------------------------------------------------------------------------- 1 | 2 | 35EADGBEECEGCG -------------------------------------------------------------------------------- /docs/source/assets/C_M/C_M_position_3.svg: -------------------------------------------------------------------------------- 1 | 2 | 357EADGBEECEGEE -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from fretboardgtr.utils import ( 4 | _contains_duplicates, 5 | chromatic_position_from_root, 6 | chromatics_from_root, 7 | note_to_interval, 8 | note_to_interval_name, 9 | scale_to_enharmonic, 10 | scale_to_flat, 11 | scale_to_intervals, 12 | scale_to_sharp, 13 | sort_scale, 14 | to_flat_note, 15 | to_sharp_note, 16 | ) 17 | 18 | 19 | def test_contains_duplicates_false(): 20 | elements = ["a", "b", "c"] 21 | assert _contains_duplicates(elements) is False 22 | 23 | 24 | def test_contains_duplicates_true(): 25 | elements = ["a", "b", "c", "a"] 26 | assert _contains_duplicates(elements) is True 27 | 28 | 29 | def test_chromatics_from_root_a(): 30 | root = "A" 31 | results = ["A", "A#", "B", "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#"] 32 | assert results == chromatics_from_root(root) 33 | 34 | 35 | def test_chromatics_from_root_c(): 36 | root = "C" 37 | results = [ 38 | "C", 39 | "C#", 40 | "D", 41 | "D#", 42 | "E", 43 | "F", 44 | "F#", 45 | "G", 46 | "G#", 47 | "A", 48 | "A#", 49 | "B", 50 | ] 51 | assert results == chromatics_from_root(root) 52 | 53 | 54 | def test_chromatics_position_from_root(): 55 | assert 3 == chromatic_position_from_root(root="A", note="C") 56 | assert 7 == chromatic_position_from_root(root="C", note="G") 57 | 58 | 59 | def test_to_sharp_note_a(): 60 | assert "A" == to_sharp_note("A") 61 | 62 | 63 | def test_to_sharp_note_a_sharp(): 64 | assert "A#" == to_sharp_note("Bb") 65 | 66 | 67 | def test_to_flat_note_a(): 68 | assert "A" == to_flat_note("A") 69 | 70 | 71 | def test_to_flat_note_a_sharp(): 72 | assert "Bb" == to_flat_note("A#") 73 | 74 | 75 | def test_scale_to_sharp(): 76 | scale = ["A", "Bb", "B", "C", "C#"] 77 | assert ["A", "A#", "B", "C", "C#"] == scale_to_sharp(scale) 78 | 79 | 80 | def test_scale_to_flat(): 81 | scale = ["A", "Bb", "B", "C", "C#"] 82 | assert ["A", "Bb", "B", "C", "Db"] == scale_to_flat(scale) 83 | 84 | 85 | def test_note_to_interval(): 86 | root = "C" 87 | note = "G" 88 | assert 7 == note_to_interval(note, root) 89 | 90 | 91 | def test_note_to_interval_name(): 92 | root = "C" 93 | note = "G" 94 | assert "5" == note_to_interval_name(note, root) 95 | 96 | 97 | def test_scale_to_intervals(): 98 | scale = ["C", "E", "G"] 99 | assert [0, 4, 7] == scale_to_intervals(scale, root="C") 100 | 101 | 102 | def test_sort_scale(): 103 | scale = [ 104 | "C", 105 | "D", 106 | "G", 107 | "C#", 108 | "Db", 109 | "F#", 110 | "A", 111 | "Ab", 112 | ] 113 | assert [ 114 | "Ab", 115 | "A", 116 | "C", 117 | "C#", 118 | "Db", 119 | "D", 120 | "F#", 121 | "G", 122 | ] == sort_scale(scale) 123 | 124 | 125 | # fmt: off 126 | @pytest.mark.parametrize( 127 | "scale, expected_result", 128 | [ 129 | ( 130 | ["A#", "C", "D", "D#", "F", "G", "A"], 131 | ["Bb", "C", "D", "Eb", "F", "G", "A"], 132 | ), 133 | ( 134 | ["A", "A#", "B", "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#"], 135 | ["A", "A#", "B", "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#"], 136 | ), 137 | ( 138 | ["A", "B", "C", "D", "E", "Gb", "G"], 139 | ["A", "B", "C", "D", "E", "F#", "G"], 140 | ), 141 | ( 142 | ["F#", "G#", "A#", "B", "C#", "D#", "F"], 143 | ["Gb", "Ab", "Bb", "Cb", "Db", "Eb", "F"], 144 | ), 145 | ( 146 | ["A#", "C", "D", "D#", "F", "G", "A"], 147 | ["Bb", "C", "D", "Eb", "F", "G", "A"], 148 | ), 149 | ( 150 | ["A", "A#", "B", "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#"], 151 | ["A", "A#", "B", "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#"], 152 | ), 153 | ( 154 | ["F#", "G#", "A#", "B", "C#", "D#", "F"], 155 | ["Gb", "Ab", "Bb", "Cb", "Db", "Eb", "F"], 156 | ), 157 | ( 158 | ["C", "D", "D#", "F", "G", "A", "A#"], 159 | ["C", "D", "Eb", "F", "G", "A", "Bb"], 160 | ), 161 | ], 162 | ) 163 | def test_scale_to_enharmonic(scale, expected_result): 164 | assert scale_to_enharmonic(scale) == expected_result 165 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | 16 | sys.path.insert(0, os.path.abspath("../..")) 17 | 18 | from docs.scripts.plot import make_plots 19 | 20 | BASE_URL = "https://github.com/antscloud/fretboardgtr" 21 | # -- Project information ----------------------------------------------------- 22 | 23 | project = "fretboardgtr" 24 | copyright = "Antoine Gibek" 25 | author = "Antscloud" 26 | 27 | LOGO_PATH = os.path.join(os.path.dirname(__file__), "assets/black_logo_square@2x.png") 28 | # -- General configuration --------------------------------------------------- 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | "sphinx.ext.autodoc", 35 | "sphinx.ext.autosummary", 36 | "sphinx.ext.napoleon", 37 | "sphinx.ext.linkcode", 38 | "myst_parser", 39 | ] 40 | 41 | 42 | def linkcode_resolve(domain, info): 43 | if domain != "py": 44 | return None 45 | if not info["module"]: 46 | return None 47 | filename = info["module"].replace(".", "/") 48 | return f"{BASE_URL}/tree/main/{filename}.py" 49 | 50 | 51 | autosummary_generate = True 52 | 53 | napoleon_google_docstring = True 54 | napoleon_numpy_docstring = True 55 | napoleon_include_init_with_doc = False 56 | napoleon_include_private_with_doc = False 57 | napoleon_include_special_with_doc = True 58 | napoleon_use_admonition_for_examples = False 59 | napoleon_use_admonition_for_notes = False 60 | napoleon_use_admonition_for_references = False 61 | napoleon_use_ivar = False 62 | napoleon_use_param = True 63 | napoleon_use_rtype = True 64 | napoleon_preprocess_types = False 65 | napoleon_type_aliases = None 66 | napoleon_attr_annotations = True 67 | myst_heading_anchors = 3 68 | 69 | autodoc_default_options = { 70 | "members": True, 71 | "show-inheritance": True, 72 | } 73 | 74 | source_suffix = { 75 | ".rst": "restructuredtext", 76 | ".txt": "markdown", 77 | ".md": "markdown", 78 | } 79 | 80 | # -- Options for autodoc ---------------------------------------------------- 81 | # https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#configuration 82 | 83 | # Automatically extract typehints when specified and place them in 84 | # descriptions of the relevant function/method. 85 | autodoc_typehints = "description" 86 | 87 | # Don't show class signature with the class' name. 88 | autodoc_class_signature = "separated" 89 | 90 | # Add any paths that contain templates here, relative to this directory. 91 | templates_path = ["_templates"] 92 | 93 | # List of patterns, relative to source directory, that match files and 94 | # directories to ignore when looking for source files. 95 | # This pattern also affects html_static_path and html_extra_path. 96 | exclude_patterns = [] 97 | 98 | html_sidebars = { 99 | "**": [ 100 | "navbar-logo.html", 101 | "search-field.html", 102 | "sbt-sidebar-nav.html", 103 | ] 104 | } 105 | 106 | # The theme to use for HTML and HTML Help pages. See the documentation for 107 | # a list of builtin themes. 108 | # 109 | html_theme = "sphinx_book_theme" 110 | 111 | html_theme_options = { 112 | "repository_url": BASE_URL, 113 | "use_repository_button": True, 114 | "use_issues_button": True, 115 | "use_download_button": True, 116 | "use_sidenotes": True, 117 | "home_page_in_toc": False, 118 | "show_toc_level": 2, 119 | } 120 | 121 | # Add any paths that contain custom static files (such as style sheets) here, 122 | # relative to this directory. They are copied after the builtin static files, 123 | # so a file named "default.css" will overwrite the builtin "default.css". 124 | html_static_path = ["assets", "_static"] 125 | 126 | html_css_files = [ 127 | "css/custom.css", 128 | ] 129 | 130 | html_logo = LOGO_PATH 131 | html_favicon = LOGO_PATH 132 | html_title = "FretBoardGtr" 133 | 134 | # -- Custom code to run ------------------------------------------------- 135 | 136 | make_plots() 137 | -------------------------------------------------------------------------------- /docs/source/assets/C_M/C_M_position_2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3579EADGBEECGCXX -------------------------------------------------------------------------------- /docs/source/assets/A_Minorpentatonic/A_Minorpentatonic_position_6.svg: -------------------------------------------------------------------------------- 1 | 2 | 35791215EBGDAEGDCG -------------------------------------------------------------------------------- /fretboardgtr/notes.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | 3 | from fretboardgtr.constants import FLAT_CHROMATICS_NOTES, SHARP_CHROMATICS_NOTES 4 | 5 | 6 | class Note: 7 | _BASE_NOTES = ["A", "B", "C", "D", "E", "F", "G"] 8 | _BASE_NOTES_DISTANCE = [2, 2, 1, 2, 2, 1, 2] 9 | 10 | def __init__(self, name: str): 11 | self.name = name 12 | if not self.check_if_valid(): 13 | raise ValueError(f"{name} is not a valid note") 14 | 15 | def __str__(self) -> str: 16 | return self.name 17 | 18 | def __repr__(self) -> str: 19 | return self.name 20 | 21 | def base_note(self) -> str: 22 | return self.name[0] 23 | 24 | def _resolve(self, prefer_flat: bool = True) -> str: 25 | if len(self.name) == 1: 26 | return self.name 27 | 28 | chromatic_scale = ( 29 | FLAT_CHROMATICS_NOTES if prefer_flat else SHARP_CHROMATICS_NOTES 30 | ) 31 | if self.name in chromatic_scale: 32 | return self.name 33 | 34 | base_note, alterations = self.name[0:1], self.name[1:] 35 | new_note = base_note 36 | if "b" in alterations: 37 | new_note = chromatic_scale[ 38 | (chromatic_scale.index(base_note) - (len(alterations))) % 12 39 | ] 40 | elif "#" in alterations: 41 | new_note = chromatic_scale[ 42 | (chromatic_scale.index(base_note) + (len(alterations))) % 12 43 | ] 44 | 45 | return new_note 46 | 47 | def resolve(self, prefer_flat: bool = True) -> "Note": 48 | """Resolve alterations in notes.""" 49 | return Note(self._resolve(prefer_flat)) 50 | 51 | def check_if_valid(self) -> bool: 52 | resolved_note = self._resolve() 53 | return ( 54 | resolved_note in SHARP_CHROMATICS_NOTES 55 | or resolved_note in FLAT_CHROMATICS_NOTES 56 | ) 57 | 58 | def sharpen(self) -> "Note": 59 | """Sharpen the note name by increasing the pitch by one semitone. 60 | 61 | Returns 62 | ------- 63 | Note 64 | The sharpened note 65 | """ 66 | # If the note name has only one letter, just add a "#" at the end 67 | if len(self.name) == 1: 68 | return Note(self.name + "#") 69 | 70 | base_note, alterations = self.name[0:1], self.name[1:] 71 | 72 | # If the note has a flat alteration, remove it 73 | if "b" in alterations: 74 | return Note(base_note + alterations[:-1]) 75 | 76 | if "#" in alterations: 77 | return Note(self.name + "#") 78 | 79 | return self 80 | 81 | def flatten(self) -> "Note": 82 | """Flatten the note name by decreasing the pitch by one semitone. 83 | 84 | Returns 85 | ------- 86 | Note 87 | The flattened note 88 | """ 89 | # If the note has only one character, just add 'b' to it 90 | if len(self.name) == 1: 91 | return Note(self.name + "b") 92 | 93 | base_note, alterations = self.name[0:1], self.name[1:] 94 | 95 | if "b" in alterations: 96 | return Note(self.name + "b") 97 | 98 | # If the note has '#' in its alterations, remove the sharp 99 | if "#" in alterations: 100 | return Note(base_note + alterations[:-1]) 101 | 102 | return self 103 | 104 | def __next_base_note(self, base_note: str) -> Tuple[str, int]: 105 | idx_base_note = self._BASE_NOTES.index(base_note) 106 | next_idx = (idx_base_note + 1) % 7 107 | return self._BASE_NOTES[next_idx], self._BASE_NOTES_DISTANCE[next_idx] 108 | 109 | def __previous_base_note(self, base_note: str) -> Tuple[str, int]: 110 | idx_base_note = self._BASE_NOTES.index(base_note) 111 | next_idx = (idx_base_note - 1) % 7 112 | # We add 1 to the note distance because as we're going backward, 113 | # we need to use the n+1 interval 114 | return self._BASE_NOTES[next_idx], self._BASE_NOTES_DISTANCE[(next_idx + 1) % 7] 115 | 116 | def flat_enharmonic(self) -> "Note": 117 | """Transform the note into its enharmonic flat equivalent. 118 | 119 | If the note has alterations, it retains them and applies flats 120 | instead of sharps. 121 | 122 | Returns 123 | ------- 124 | Note 125 | The enharmonic equivalent note with flats. 126 | """ 127 | # Get the base note and any alterations 128 | base_note = self.name[0:1] 129 | alterations = self.name[1:] if len(self.name) >= 2 else None 130 | 131 | # Apply alterations 132 | if alterations is not None and "#" in alterations: 133 | return self.resolve(prefer_flat=True) 134 | 135 | target_note, distance = self.__next_base_note(base_note) 136 | 137 | if alterations is not None and "b" in alterations: 138 | return Note(target_note + alterations + "b" * distance) 139 | else: 140 | flats_to_add = "b" * distance 141 | return Note(target_note + flats_to_add) 142 | 143 | def sharp_enharmonic(self) -> "Note": 144 | """Transform the note into its enharmonic sharp equivalent. 145 | 146 | If the note has alterations, it retains them and applies sharps 147 | instead of flats. 148 | 149 | Returns 150 | ------- 151 | Note 152 | The enharmonic equivalent note with sharps. 153 | """ 154 | # Get the base note and any alterations 155 | base_note = self.name[0:1] 156 | alterations = self.name[1:] if len(self.name) >= 2 else None 157 | 158 | # Apply alterations 159 | if alterations is not None and "b" in alterations: 160 | return self.resolve(prefer_flat=False) 161 | 162 | target_note, distance = self.__previous_base_note(base_note) 163 | 164 | if alterations is not None and "#" in alterations: 165 | return Note(target_note + alterations + "#" * distance) 166 | else: 167 | sharps_to_add = "#" * distance 168 | return Note(target_note + sharps_to_add) 169 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome, and they are greatly appreciated! Every little bit 4 | helps, and credit will always be given. 5 | 6 | You can contribute in many ways: 7 | 8 | ## Types of Contributions 9 | 10 | 11 | ### Report Bugs 12 | 13 | Report bugs at https://github.com/antscloud/fretboardgtr/issues. 14 | 15 | If you are reporting a bug, please include: 16 | 17 | * Your operating system name and version. 18 | * Any details about your local setup that might be helpful in troubleshooting. 19 | * Detailed steps to reproduce the bug. 20 | 21 | ### Fix Bugs 22 | 23 | Look through the GitHub issues for bugs. Anything tagged with "bug" and "help 24 | wanted" is open to whoever wants to implement it. 25 | 26 | ### Implement Features 27 | 28 | Look through the GitHub issues for features. Anything tagged with "enhancement" 29 | and "help wanted" is open to whoever wants to implement it. 30 | 31 | ### Write Documentation 32 | 33 | FretBoardGtr could always use more documentation, whether as part of the 34 | official FretBoardGtr docs, in docstrings, or even on the web in blog posts, 35 | articles, and such. 36 | 37 | ### Submit Feedback 38 | 39 | The best way to send feedback is to file an issue at https://github.com/antscloud/fretboardgtr/issues. 40 | 41 | If you are proposing a feature: 42 | 43 | * Explain in detail how it would work. 44 | * Keep the scope as narrow as possible, to make it easier to implement. 45 | * Remember that this is a volunteer-driven project, and that contributions 46 | are welcome :) 47 | 48 | ## Get Started! 49 | 50 | Ready to contribute? Here's how to set up `fretboardgtr` for local development. 51 | 52 | 1. Fork the `fretboardgtr` repo on GitHub. 53 | 2. Clone your fork locally: 54 | 55 | $ git clone git@github.com:your_name_here/fretboardgtr.git 56 | 57 | 3. Install your local copy into a virtual environment. 58 | 1. With **`venv`** : Assuming you have `venv` installed, this is how you set up your fork for local development : 59 | 60 | ``` 61 | cd fretboardgtr/ 62 | python -m venv 63 | pip install -e .[dev] 64 | ``` 65 | 66 | 2. With **`conda`** : Assuming you have conda installed, this is how you set up your fork for local development : 67 | 68 | ``` 69 | cd fretboardgtr/ 70 | conda create -n python=3.10 71 | conda activate 72 | pip install -e .[dev] 73 | ``` 74 | 75 | 4. Install the pre-commit hooks by running : 76 | ``` 77 | pre-commit install 78 | ``` 79 | 80 | 5. Make sure you have `git-lfs` installed, then run `git lfs install`. 81 | 82 | 6. Create a branch for local development: 83 | 84 | ``` 85 | git checkout -b name-of-your-bugfix-or-feature 86 | ``` 87 | 88 | Now you can make your changes locally. 89 | 90 | 7. When you're done making changes, apply the pre-commit and check that your changes pass the 91 | tests, including testing other Python versions. 92 | 93 | ``` 94 | pre-commit fretboardgtr tests 95 | python -m pytest 96 | ``` 97 | 98 | 8. If files are modified by the pre-commit hooks, you need to rea-add them : 99 | ``` 100 | git add 101 | ``` 102 | 103 | 9. Commit your changes and push your branch to GitHub: 104 | 105 | ``` 106 | git add . 107 | git commit -m "Your detailed description of your changes." 108 | git push origin name-of-your-bugfix-or-feature 109 | ``` 110 | 111 | 10. Submit a pull request through the GitHub website. 112 | 113 | ## Pull Request Guidelines 114 | 115 | Before you submit a pull request, check that it meets these guidelines: 116 | 117 | 1. The pull request should include tests. 118 | 2. If the pull request adds functionality, the docs should be updated. Put 119 | your new functionality into a function with a docstring, and add the 120 | feature to the list in README.rst. 121 | 3. The pull request should work for Python 3.5, 3.6, 3.7 and 3.8, and for PyPy. Check 122 | https://github.com/antscloud/fretboardgtr/pull_requests 123 | and make sure that the tests pass for all supported Python versions. 124 | 125 | ## Tips 126 | 127 | To run a subset of tests : 128 | ``` 129 | python -m pytest tests.test_fretboardgtr 130 | ``` 131 | ## Deploying 132 | 133 | A reminder for the maintainers on how to deploy. 134 | Make sure all your changes are committed. 135 | 136 | Go to fretboardgtr/_version.py and bump the version to the version you want. 137 | Let's say from `0.2.0` to `0.2.1. 138 | 139 | 140 | ```diff 141 | < version_str = "0.2.0" 142 | > version_str = "0.2.1" 143 | 144 | ``` 145 | 146 | Then commit this change 147 | 148 | ```sh 149 | git commit -m "Bump version 0.2.0 to 0.2.1" -m "" -m "More descriptive message" 150 | ``` 151 | 152 | ```sh 153 | git tag -a "0.2.1" -m "Bump version 0.2.0 to 0.2.1" -m "" -m "More descriptive message" 154 | ``` 155 | 156 | ``` 157 | git push origin master --tags 158 | ``` 159 | 160 | This will then deploy the package in PyPI if tests pass and a tag is set, otherwise it will deployed on test-pipy. 161 | 162 | ## Changelogs 163 | The changelogs are generated using [git cliff][https://git-cliff.org/]. This is a rust utility tool that can be downloaded with `cargo`. 164 | 165 | The configuration file for this tool is `cliff.toml`. 166 | 167 | After modifying the `_version.py` file as described above please simply run : 168 | ``` 169 | git cliff -o CHANGELOG.md 170 | git add CHANGELOG.md 171 | git commit -m "docs: Auto-generated changelog" 172 | git push origin master 173 | ``` 174 | 175 | ## Further words 176 | 177 | ### `pre-commit` 178 | 179 | Most of the pre-commit hooks require you to do nothing but save the changes. However, some pre-commits (e.g. `pydocstyle`) are sometimes hard to respect and can slow down your workflow. Although we recommend to let all of them to have a cleaner repo, if one or more are really annoying for you, you can remove or comment them in the `.pre-commit-config.yaml` file. 180 | 181 | Before each commit, each hook is going to run against file in the staged area (files added with `git add`). Some of the hooks may modify the files, if this happened, the commit is cancelled. You need to re-add the file(s) modified by running `git add ` and recommit. 182 | 183 | ### Conventional commits 184 | 185 | Although it is not mandatory, we suggest you to use the [Conventional Commit](https://www.conventionalcommits.org/en/v1.0.0/) conventions to write commit messages. 186 | -------------------------------------------------------------------------------- /docs/source/assets/A_Minorpentatonic/A_Minorpentatonic_position_3.svg: -------------------------------------------------------------------------------- 1 | 2 | 35791215EBGDAECDGAECGCD -------------------------------------------------------------------------------- /docs/source/assets/A_Minorpentatonic/A_Minorpentatonic_position_1.svg: -------------------------------------------------------------------------------- 1 | 2 | 35791215EBGDAEGADECGCDGA -------------------------------------------------------------------------------- /docs/source/get-started/get-started.md: -------------------------------------------------------------------------------- 1 | # Get started 2 | 3 | To get started simply install the package from PyPI 4 | 5 | ## How to install 6 | 7 | `fretboardgtr` needs to have the following install in order to run : 8 | 9 | ```shell 10 | sudo apt install libcairo2-dev pkg-config 11 | ``` 12 | 13 | ```shell 14 | pip install fretboardgtr 15 | ``` 16 | 17 | ## Usage 18 | 19 | ```python 20 | from fretboardgtr.fretboard import FretBoard 21 | from fretboardgtr.notes_creators import ScaleFromName 22 | 23 | fretboard = FretBoard() 24 | c_major = ScaleFromName(root="C", mode="Ionian").build() 25 | fretboard.add_notes(scale=c_major) 26 | fretboard.export("my_fretboard.svg", format="svg") 27 | ``` 28 | 29 | ![My Fretboard](../assets/my_fretboard.svg) 30 | ## Customization example 31 | 32 | ```python 33 | from fretboardgtr.fretboard import FretBoard, FretBoardConfig 34 | from fretboardgtr.notes_creators import ScaleFromName 35 | 36 | config = { 37 | "general": { 38 | "first_fret": 0, 39 | "last_fret": 24, 40 | "show_tuning": False, 41 | "show_frets": True, 42 | "show_note_name": False, 43 | "show_degree_name": True, 44 | "open_color_scale": True, 45 | "fretted_color_scale": True, 46 | "fretted_colors": { 47 | "root": "rgb(255,255,255)", 48 | }, 49 | "open_colors": { 50 | "root": "rgb(255,255,255)", 51 | }, 52 | "enharmonic": True, 53 | }, 54 | "background": {"color": "rgb(0,0,50)", "opacity": 0.4}, 55 | "frets": {"color": "rgb(150,150,150)"}, 56 | "fret_numbers": {"color": "rgb(150,150,150)", "fontsize": 20, "fontweight": "bold"}, 57 | "strings": {"color": "rgb(200,200,200)", "width": 2}, 58 | } 59 | 60 | fretboard_config = FretBoardConfig.from_dict(config) 61 | fretboard = FretBoard(config=fretboard_config) 62 | c_major = ScaleFromName(root="A", mode="Ionian").build() 63 | fretboard.add_notes(scale=c_major) 64 | fretboard.export("my_custom_fretboard.svg", format="svg") 65 | ``` 66 | 67 | 68 | ![My custom Fretboard](../assets/my_custom_fretboard.svg) 69 | Please see the [configuration documentation](./configuration.md) for more details. 70 | 71 | 72 | ## Vertical Fretboard 73 | ```python 74 | from fretboardgtr.fretboard import FretBoard 75 | from fretboardgtr.notes_creators import ScaleFromName 76 | 77 | fretboard = FretBoard(vertical=True) 78 | c_major = ScaleFromName(root="C", mode="Ionian").build() 79 | fretboard.add_notes(scale=c_major) 80 | fretboard.export("my_vertical_fretboard.svg", format="svg") 81 | ``` 82 | 83 | ```{image} ../assets/my_vertical_fretboard.svg 84 | :alt: My vertical fretboard 85 | :width: 200px 86 | :align: center 87 | ``` 88 | ## Examples 89 | 90 | ### Draw a chord diagram 91 | 92 | ```python 93 | from fretboardgtr.fretboard import FretBoardConfig, FretBoard 94 | 95 | config = { 96 | "general": { 97 | "first_fret": 0, 98 | "last_fret": 5, 99 | "fret_width": 50, 100 | } 101 | } 102 | fretboard_config = FretBoardConfig.from_dict(config) 103 | fretboard = FretBoard(config=fretboard_config, vertical=True) 104 | c_major = [0, 3, 2, 0, 1, 0] 105 | 106 | fretboard.add_fingering(c_major, root="C") 107 | fretboard.export("my_vertical_fretboard.svg", format="svg") 108 | ``` 109 | 110 | ```{image} ../assets/c_major_chord.svg 111 | :alt: My vertical fretboard 112 | :width: 200px 113 | :align: center 114 | ``` 115 | 116 | ### Draw all propably possible chord position for a specific chord 117 | 118 | ⚠️ Be careful with this snippets. This example generates over 1000 svgs 119 | ```python 120 | from fretboardgtr.fretboard import FretBoardConfig, FretBoard 121 | from fretboardgtr.constants import Chord 122 | from fretboardgtr.notes_creators import ChordFromName 123 | 124 | TUNING = ["E", "A", "D", "G", "B", "E"] 125 | ROOT = "C" 126 | QUALITY = ChordName.MAJOR 127 | 128 | fingerings = ( 129 | ChordFromName(root=ROOT, quality=QUALITY).build().get_chord_fingerings(TUNING) 130 | ) 131 | for i, fingering in enumerate(fingerings): 132 | _cleaned_fingering = [pos for pos in fingering if pos is not None and pos != 0] 133 | first_fret = min(_cleaned_fingering) - 2 134 | if first_fret < 0: 135 | first_fret = 0 136 | 137 | last_fret = max(_cleaned_fingering) + 2 138 | if last_fret < 4: 139 | last_fret = 4 140 | 141 | config = { 142 | "general": { 143 | "first_fret": first_fret, 144 | "last_fret": last_fret, 145 | "fret_width": 50, 146 | } 147 | } 148 | fretboard_config = FretBoardConfig.from_dict(config) 149 | fretboard = FretBoard(config=fretboard_config, tuning=TUNING, vertical=True) 150 | fretboard.add_fingering(fingering, root=ROOT) 151 | fretboard.export( 152 | f"./{ROOT}_{QUALITY.value}/{ROOT}_{QUALITY.value}_position_{i}.svg", 153 | format="svg", 154 | ) 155 | 156 | 157 | ``` 158 | 159 | Will give you : 160 | 161 | ```{image} ../assets/C_M/C_M_position_0.svg 162 | :alt: My vertical fretboard 163 | :width: 200px 164 | :align: center 165 | ``` 166 | 167 | 168 | ```{image} ../assets/C_M/C_M_position_1.svg 169 | :alt: My vertical fretboard 170 | :width: 200px 171 | :align: center 172 | ``` 173 | 174 | 175 | ```{image} ../assets/C_M/C_M_position_2.svg 176 | :alt: My vertical fretboard 177 | :width: 200px 178 | :align: center 179 | ``` 180 | 181 | ```{image} ../assets/C_M/C_M_position_3.svg 182 | :alt: My vertical fretboard 183 | :width: 200px 184 | :align: center 185 | ``` 186 | And so on. 187 | 188 | ### Generate all the classic positions for A minor pentatonic scale 189 | 190 | ```python 191 | from fretboardgtr.fretboard import FretBoardConfig, FretBoard 192 | from fretboardgtr.notes_creators import ScaleFromName 193 | from fretboardgtr.constants import ModeName 194 | 195 | TUNING = ["E", "A", "D", "G", "B", "E"] 196 | ROOT = "A" 197 | MODE = ModeName.MINOR_PENTATONIC 198 | 199 | scale_positions = ( 200 | ScaleFromName(root=ROOT, mode=MODE).build().get_scale_positions(TUNING, max_spacing=4) 201 | ) 202 | config = { 203 | "general": { 204 | "last_fret": 16, 205 | } 206 | } 207 | 208 | for i, scale_position in enumerate(scale_positions): 209 | fretboard = FretBoard(config=config, tuning=TUNING) 210 | fretboard.add_scale(scale_position, root=ROOT) 211 | fretboard.export( 212 | f"./{ROOT}_{MODE.value}/{ROOT}_{MODE.value}_position_{i}.svg", format="svg" 213 | ) 214 | ``` 215 | 216 | Will give you : 217 | 218 | ```{image} ../assets/A_Minorpentatonic/A_Minorpentatonic_position_0.svg 219 | :alt: My vertical fretboard 220 | :width: 80% 221 | :align: center 222 | ``` 223 | 224 | 225 | ```{image} ../assets/A_Minorpentatonic/A_Minorpentatonic_position_1.svg 226 | :alt: My vertical fretboard 227 | :width: 80% 228 | :align: center 229 | ``` 230 | 231 | 232 | ```{image} ../assets/A_Minorpentatonic/A_Minorpentatonic_position_2.svg 233 | :alt: My vertical fretboard 234 | :width: 80% 235 | :align: center 236 | ``` 237 | 238 | And so on. 239 | -------------------------------------------------------------------------------- /docs/source/assets/A_Minorpentatonic/A_Minorpentatonic_position_4.svg: -------------------------------------------------------------------------------- 1 | 2 | 35791215EBGDAEDEACGCDGADE -------------------------------------------------------------------------------- /tests/test_e2e.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | 4 | from fretboardgtr.fretboard import FretBoard, FretBoardConfig 5 | from fretboardgtr.notes_creators import ScaleFromName 6 | 7 | C_IONIAN_SVG = Path(__file__).parent / "data" / "c_ionian.svg" 8 | OUTPUTS_ARTIFACT_FOLDER = Path(__file__).parent / "data" / "outputs" / "e2e" 9 | 10 | 11 | def remove_test_file(path: Path): 12 | if path.exists(): 13 | os.remove(str(path)) 14 | 15 | 16 | def test_c_major_fretboard(): 17 | fretboard = FretBoard() 18 | c_major = ScaleFromName(root="C", mode="Ionian").build() 19 | fretboard.add_notes(scale=c_major) 20 | os.makedirs(OUTPUTS_ARTIFACT_FOLDER, exist_ok=True) 21 | tmp_file = OUTPUTS_ARTIFACT_FOLDER / "horizontal.svg" 22 | remove_test_file(tmp_file) 23 | assert not tmp_file.exists() 24 | fretboard.export(tmp_file) 25 | assert tmp_file.exists() 26 | 27 | 28 | def test_c_major_vertical_fretboard(): 29 | fretboard = FretBoard(vertical=True) 30 | c_major = ScaleFromName(root="C", mode="Ionian").build() 31 | fretboard.add_notes(scale=c_major) 32 | os.makedirs(OUTPUTS_ARTIFACT_FOLDER, exist_ok=True) 33 | tmp_file = OUTPUTS_ARTIFACT_FOLDER / "vertical.svg" 34 | remove_test_file(tmp_file) 35 | assert not tmp_file.exists() 36 | fretboard.export(tmp_file) 37 | assert tmp_file.exists() 38 | 39 | 40 | def test_c_major_fretboard_starting_at_5(): 41 | config = { 42 | "general": { 43 | "first_fret": 5, 44 | "last_fret": 12, 45 | }, 46 | } 47 | 48 | fretboard_config = FretBoardConfig.from_dict(config) 49 | fretboard = FretBoard(config=fretboard_config) 50 | c_major = ScaleFromName(root="C", mode="Ionian").build() 51 | fretboard.add_notes(scale=c_major) 52 | os.makedirs(OUTPUTS_ARTIFACT_FOLDER, exist_ok=True) 53 | tmp_file = OUTPUTS_ARTIFACT_FOLDER / "horizontal_starting_at_5.svg" 54 | remove_test_file(tmp_file) 55 | assert not tmp_file.exists() 56 | fretboard.export(tmp_file) 57 | assert tmp_file.exists() 58 | 59 | 60 | def test_c_major_vertical_fretboard_starting_at_5(): 61 | config = { 62 | "general": { 63 | "first_fret": 5, 64 | "last_fret": 12, 65 | }, 66 | } 67 | 68 | fretboard_config = FretBoardConfig.from_dict(config) 69 | fretboard = FretBoard(config=fretboard_config, vertical=True) 70 | c_major = ScaleFromName(root="C", mode="Ionian").build() 71 | fretboard.add_notes(scale=c_major) 72 | os.makedirs(OUTPUTS_ARTIFACT_FOLDER, exist_ok=True) 73 | tmp_file = OUTPUTS_ARTIFACT_FOLDER / "vertical_starting_at_5.svg" 74 | remove_test_file(tmp_file) 75 | assert not tmp_file.exists() 76 | fretboard.export(tmp_file) 77 | assert tmp_file.exists() 78 | 79 | 80 | def test_c_major_fingering(): 81 | config = { 82 | "general": { 83 | "first_fret": 0, 84 | "last_fret": 5, 85 | "fret_width": 50, 86 | } 87 | } 88 | fretboard_config = FretBoardConfig.from_dict(config) 89 | fretboard = FretBoard(config=fretboard_config) 90 | c_major = [0, 3, 2, 0, 1, 0] 91 | 92 | fretboard.add_fingering(c_major, root="C") 93 | os.makedirs(OUTPUTS_ARTIFACT_FOLDER, exist_ok=True) 94 | tmp_file = OUTPUTS_ARTIFACT_FOLDER / "c_major_fingering.svg" 95 | remove_test_file(tmp_file) 96 | assert not tmp_file.exists() 97 | fretboard.export(tmp_file) 98 | assert tmp_file.exists() 99 | 100 | 101 | def test_c_major_vertical_fingering(): 102 | config = { 103 | "general": { 104 | "first_fret": 1, 105 | "last_fret": 5, 106 | "fret_width": 50, 107 | } 108 | } 109 | fretboard_config = FretBoardConfig.from_dict(config) 110 | fretboard = FretBoard(config=fretboard_config, vertical=True) 111 | c_major = [0, 3, 2, 0, 1, 0] 112 | 113 | fretboard.add_fingering(c_major, root="C") 114 | os.makedirs(OUTPUTS_ARTIFACT_FOLDER, exist_ok=True) 115 | tmp_file = OUTPUTS_ARTIFACT_FOLDER / "c_major_vertical_fingering.svg" 116 | remove_test_file(tmp_file) 117 | assert not tmp_file.exists() 118 | fretboard.export(tmp_file) 119 | assert tmp_file.exists() 120 | 121 | 122 | def test_a_major_pentatonic_scale(): 123 | config = { 124 | "general": { 125 | "first_fret": 1, 126 | "last_fret": 12, 127 | "fret_width": 50, 128 | } 129 | } 130 | fretboard_config = FretBoardConfig.from_dict(config) 131 | fretboard = FretBoard(config=fretboard_config) 132 | a_major_pentatonic = [[5, 8], [5, 7], [5, 7], [5, 7], [5, 8], [5, 8]] 133 | 134 | fretboard.add_scale(a_major_pentatonic, root="A") 135 | os.makedirs(OUTPUTS_ARTIFACT_FOLDER, exist_ok=True) 136 | tmp_file = OUTPUTS_ARTIFACT_FOLDER / "a_major_pentatonic.svg" 137 | remove_test_file(tmp_file) 138 | assert not tmp_file.exists() 139 | fretboard.export(tmp_file) 140 | assert tmp_file.exists() 141 | 142 | 143 | def test_a_major_vertical_pentatonic_scale(): 144 | config = { 145 | "general": { 146 | "first_fret": 1, 147 | "last_fret": 12, 148 | "fret_width": 50, 149 | } 150 | } 151 | fretboard_config = FretBoardConfig.from_dict(config) 152 | fretboard = FretBoard(config=fretboard_config, vertical=True) 153 | a_major_pentatonic = [[5, 8], [5, 7], [5, 7], [5, 7], [5, 8], [5, 8]] 154 | 155 | fretboard.add_scale(a_major_pentatonic, root="A") 156 | os.makedirs(OUTPUTS_ARTIFACT_FOLDER, exist_ok=True) 157 | tmp_file = OUTPUTS_ARTIFACT_FOLDER / "a_major_pentatonic_vertical.svg" 158 | remove_test_file(tmp_file) 159 | assert not tmp_file.exists() 160 | fretboard.export(tmp_file) 161 | assert tmp_file.exists() 162 | 163 | 164 | def test_c_major_fretboard_no_nut(): 165 | config = { 166 | "general": { 167 | "show_nut": False, 168 | } 169 | } 170 | fretboard_config = FretBoardConfig.from_dict(config) 171 | fretboard = FretBoard(config=fretboard_config) 172 | c_major = ScaleFromName(root="C", mode="Ionian").build() 173 | fretboard.add_notes(scale=c_major) 174 | os.makedirs(OUTPUTS_ARTIFACT_FOLDER, exist_ok=True) 175 | tmp_file = OUTPUTS_ARTIFACT_FOLDER / "horizontal_no_nut.svg" 176 | remove_test_file(tmp_file) 177 | assert not tmp_file.exists() 178 | fretboard.export(tmp_file) 179 | assert tmp_file.exists() 180 | 181 | 182 | def test_c_major_fretboard_neck_dot_odd(): 183 | config = { 184 | "general": { 185 | "first_fret": 3, 186 | "last_fret": 12, 187 | } 188 | } 189 | fretboard_config = FretBoardConfig.from_dict(config) 190 | fretboard = FretBoard(config=fretboard_config) 191 | c_major = ScaleFromName(root="C", mode="Ionian").build() 192 | fretboard.add_notes(scale=c_major) 193 | os.makedirs(OUTPUTS_ARTIFACT_FOLDER, exist_ok=True) 194 | tmp_file = OUTPUTS_ARTIFACT_FOLDER / "horizontal_fretboard_neck_dot_odd.svg" 195 | remove_test_file(tmp_file) 196 | assert not tmp_file.exists() 197 | fretboard.export(tmp_file) 198 | assert tmp_file.exists() 199 | -------------------------------------------------------------------------------- /docs/source/assets/A_Minorpentatonic/A_Minorpentatonic_position_0.svg: -------------------------------------------------------------------------------- 1 | 2 | 35791215EBGDAEEGCDGADEACEG -------------------------------------------------------------------------------- /docs/source/assets/A_Minorpentatonic/A_Minorpentatonic_position_2.svg: -------------------------------------------------------------------------------- 1 | 2 | 35791215EBGDAEACEGCDGADEAC -------------------------------------------------------------------------------- /docs/source/assets/A_Minorpentatonic/A_Minorpentatonic_position_5.svg: -------------------------------------------------------------------------------- 1 | 2 | 35791215EBGDAEEGCDGADEACEG -------------------------------------------------------------------------------- /fretboardgtr/fretboards/vertical.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, Optional, Tuple 2 | 3 | from fretboardgtr.constants import STANDARD_TUNING 4 | from fretboardgtr.fretboards.config import FretBoardConfig 5 | 6 | 7 | class VerticalFretBoard: 8 | """Class containing the different elements of a fretboard. 9 | 10 | Also contains associated method to add some. 11 | """ 12 | 13 | def __init__( 14 | self, 15 | tuning: Optional[List[str]] = None, 16 | config: Optional[FretBoardConfig] = None, 17 | ): 18 | self.tuning = tuning if tuning is not None else STANDARD_TUNING 19 | self.config = config if config is not None else FretBoardConfig() 20 | 21 | def get_list_in_good_order(self, _list: List[Any]) -> List[Any]: 22 | return _list 23 | 24 | def get_background_start_position(self) -> Tuple[float, float]: 25 | open_fret_height = self.config.general.fret_height 26 | return ( 27 | self.config.general.x_start, 28 | self.config.general.y_start + open_fret_height, 29 | ) 30 | 31 | def get_background_dimensions(self) -> Tuple[float, float]: 32 | # We add 1 as it is one-indexed for first fret 33 | number_of_frets = ( 34 | self.config.general.last_fret - self.config.general.first_fret 35 | ) + 1 36 | width = (len(self.tuning) - 1) * self.config.general.fret_width 37 | height = (number_of_frets) * (self.config.general.fret_height) 38 | return width, height 39 | 40 | def get_neck_dot_position(self, dot: int) -> List[Tuple[float, float]]: 41 | x = ( 42 | self.config.general.x_start 43 | + (len(self.tuning) / 2 - (1 / 2)) * self.config.general.fret_width 44 | ) 45 | y = ( 46 | self.config.general.y_start 47 | + (0.5 + dot - self.config.general.first_fret + 1) 48 | * self.config.general.fret_height 49 | ) 50 | if dot % 12 == 0: 51 | # Add two dots dot is multiple of 12 52 | lower_position = ( 53 | x - self.config.general.fret_width, 54 | y, 55 | ) 56 | upper_position = ( 57 | x + self.config.general.fret_width, 58 | y, 59 | ) 60 | return [lower_position, upper_position] 61 | else: 62 | center_position = ( 63 | x, 64 | y, 65 | ) 66 | return [center_position] 67 | 68 | def get_fret_position( 69 | self, fret_no: int 70 | ) -> Tuple[Tuple[float, float], Tuple[float, float]]: 71 | y = self.config.general.y_start + (self.config.general.fret_height) * (fret_no) 72 | y_start = self.config.general.x_start 73 | x_end = self.config.general.x_start + (self.config.general.fret_width) * ( 74 | len(self.tuning) - 1 75 | ) 76 | return (y_start, y), (x_end, y) 77 | 78 | def get_strings_position( 79 | self, string_no: int 80 | ) -> Tuple[Tuple[float, float], Tuple[float, float]]: 81 | open_fret_height = self.config.general.fret_height 82 | 83 | y_start = self.config.general.y_start + open_fret_height 84 | y_end = self.config.general.y_start + ( 85 | self.config.general.fret_height 86 | + (self.config.general.last_fret - self.config.general.first_fret + 1) 87 | * self.config.general.fret_height 88 | ) 89 | start = ( 90 | self.config.general.x_start 91 | + (self.config.general.fret_width) * (string_no), 92 | y_start, 93 | ) 94 | end = ( 95 | self.config.general.x_start 96 | + (self.config.general.fret_width) * (string_no), 97 | y_end, 98 | ) 99 | return start, end 100 | 101 | def get_nut_position( 102 | self, 103 | ) -> Optional[Tuple[Tuple[float, float], Tuple[float, float]]]: 104 | if self.config.general.first_fret != 1 or not self.config.general.show_nut: 105 | return None 106 | open_fret_height = self.config.general.fret_height 107 | 108 | start = ( 109 | self.config.general.x_start, 110 | self.config.general.y_start + open_fret_height, 111 | ) 112 | end = ( 113 | self.config.general.x_start 114 | + self.config.general.fret_width * (len(self.tuning) - 1), 115 | self.config.general.y_start + open_fret_height, 116 | ) 117 | return start, end 118 | 119 | def get_fret_number_position(self, dot: int) -> Tuple[float, float]: 120 | x = self.config.general.x_start + self.config.general.fret_width * ( 121 | len(self.tuning) 122 | ) 123 | y = self.config.general.y_start + self.config.general.fret_height * ( 124 | 1 / 2 + dot - self.config.general.first_fret + 1 125 | ) 126 | return x, y 127 | 128 | def get_tuning_position(self, string_no: int) -> Tuple[float, float]: 129 | x = self.config.general.x_start + self.config.general.fret_width * (string_no) 130 | y = self.config.general.y_start + ( 131 | self.config.general.fret_height 132 | * (self.config.general.last_fret - self.config.general.first_fret + 5 / 2) 133 | ) 134 | return x, y 135 | 136 | def get_single_note_position( 137 | self, string_no: int, index: int 138 | ) -> Tuple[float, float]: 139 | y_pos = index + (1 / 2) 140 | x = self.config.general.x_start + self.config.general.fret_width * (string_no) 141 | y = self.config.general.y_start + (self.config.general.fret_height) * y_pos 142 | return x, y 143 | 144 | def get_cross_position(self, string_no: int) -> Tuple[float, float]: 145 | x = self.config.general.x_start + (self.config.general.fret_width) * (string_no) 146 | y = self.config.general.y_start + self.config.general.fret_height * (1 / 2) 147 | return x, y 148 | 149 | def get_size(self) -> Tuple[float, float]: 150 | """Get total size of the drawing. 151 | 152 | Returns 153 | ------- 154 | Tuple[float, float] 155 | Width and heigth 156 | """ 157 | # We add 1 as it is one-indexed for first fret 158 | number_of_frets = ( 159 | self.config.general.last_fret - self.config.general.first_fret 160 | ) + 1 161 | width = ( 162 | self.config.general.x_start 163 | + self.config.general.fret_width * (len(self.tuning) + 1) 164 | + self.config.general.y_end_offset 165 | ) 166 | height = ( 167 | self.config.general.y_start 168 | + self.config.general.fret_height * (number_of_frets + 2) 169 | + self.config.general.x_end_offset 170 | ) 171 | return (width, height) 172 | 173 | def get_inside_bounds( 174 | self, 175 | ) -> Tuple[Tuple[float, float], Tuple[float, float]]: 176 | """Get the size of the inner drawing. 177 | 178 | This function could be use to add custom elements 179 | 180 | Returns 181 | ------- 182 | Tuple[Tuple[float, float], Tuple[float, float]] 183 | Upper left corner x coordinate, upper left corner y coordinate 184 | Lower right corner x coordinate, lower right corner y coordinate 185 | """ 186 | open_fret_height = self.config.general.fret_height 187 | # We add 1 as it is one-indexed for first fret 188 | number_of_frets = ( 189 | self.config.general.last_fret - self.config.general.first_fret 190 | ) + 1 191 | upper_left_x = self.config.general.x_start 192 | upper_left_y = self.config.general.y_start 193 | lower_right_x = ( 194 | self.config.general.x_start 195 | + (len(self.tuning) - 1) * self.config.general.fret_width 196 | ) 197 | lower_right_y = ( 198 | self.config.general.y_start 199 | + open_fret_height 200 | + (number_of_frets) * (self.config.general.fret_height) 201 | ) 202 | return ((upper_left_x, upper_left_y), (lower_right_x, lower_right_y)) 203 | -------------------------------------------------------------------------------- /fretboardgtr/fretboards/horizontal.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, Optional, Tuple 2 | 3 | from fretboardgtr.constants import STANDARD_TUNING 4 | from fretboardgtr.fretboards.config import FretBoardConfig 5 | 6 | 7 | class HorizontalFretBoard: 8 | """Class containing the different elements of a fretboard. 9 | 10 | Also contains associated method to add some. 11 | """ 12 | 13 | def __init__( 14 | self, 15 | tuning: Optional[List[str]] = None, 16 | config: Optional[FretBoardConfig] = None, 17 | ): 18 | self.tuning = tuning if tuning is not None else STANDARD_TUNING 19 | self.config = config if config is not None else FretBoardConfig() 20 | 21 | def get_list_in_good_order(self, _list: List[Any]) -> List[Any]: 22 | return _list[::-1] 23 | 24 | def get_background_start_position(self) -> Tuple[float, float]: 25 | open_fret_width = self.config.general.fret_width 26 | return ( 27 | self.config.general.x_start + open_fret_width, 28 | self.config.general.y_start, 29 | ) 30 | 31 | def get_background_dimensions(self) -> Tuple[float, float]: 32 | # We add 1 as it is one-indexed for first fret 33 | number_of_frets = ( 34 | self.config.general.last_fret - self.config.general.first_fret 35 | ) + 1 36 | width = (number_of_frets) * (self.config.general.fret_width) 37 | height = (len(self.tuning) - 1) * self.config.general.fret_height 38 | return width, height 39 | 40 | def get_neck_dot_position(self, dot: int) -> List[Tuple[float, float]]: 41 | x = ( 42 | self.config.general.x_start 43 | + (0.5 + dot - self.config.general.first_fret + 1) 44 | * self.config.general.fret_width 45 | ) 46 | y = ( 47 | self.config.general.y_start 48 | + (len(self.tuning) / 2 - (1 / 2)) * self.config.general.fret_height 49 | ) 50 | if dot % 12 == 0: 51 | # Add two dots dot is multiple of 12 52 | lower_position = ( 53 | x, 54 | y - self.config.general.fret_height, 55 | ) 56 | upper_position = ( 57 | x, 58 | y + self.config.general.fret_height, 59 | ) 60 | return [lower_position, upper_position] 61 | else: 62 | center_position = ( 63 | x, 64 | y, 65 | ) 66 | return [center_position] 67 | 68 | def get_fret_position( 69 | self, fret_no: int 70 | ) -> Tuple[Tuple[float, float], Tuple[float, float]]: 71 | x = self.config.general.x_start + (self.config.general.fret_width) * (fret_no) 72 | y_start = self.config.general.y_start 73 | y_end = self.config.general.y_start + (self.config.general.fret_height) * ( 74 | len(self.tuning) - 1 75 | ) 76 | return (x, y_start), (x, y_end) 77 | 78 | def get_strings_position( 79 | self, string_no: int 80 | ) -> Tuple[Tuple[float, float], Tuple[float, float]]: 81 | open_fret_width = self.config.general.fret_width 82 | 83 | x_start = self.config.general.x_start + open_fret_width 84 | x_end = self.config.general.x_start + ( 85 | self.config.general.fret_width 86 | + (self.config.general.last_fret - self.config.general.first_fret + 1) 87 | * self.config.general.fret_width 88 | ) 89 | start = ( 90 | x_start, 91 | self.config.general.y_start 92 | + (self.config.general.fret_height) * (string_no), 93 | ) 94 | end = ( 95 | x_end, 96 | self.config.general.y_start 97 | + (self.config.general.fret_height) * (string_no), 98 | ) 99 | return start, end 100 | 101 | def get_nut_position( 102 | self, 103 | ) -> Optional[Tuple[Tuple[float, float], Tuple[float, float]]]: 104 | if self.config.general.first_fret != 1 or not self.config.general.show_nut: 105 | return None 106 | open_fret_width = self.config.general.fret_width 107 | 108 | start = ( 109 | self.config.general.x_start + open_fret_width, 110 | self.config.general.y_start, 111 | ) 112 | end = ( 113 | self.config.general.x_start + open_fret_width, 114 | self.config.general.y_start 115 | + self.config.general.fret_height * (len(self.tuning) - 1), 116 | ) 117 | return start, end 118 | 119 | def get_fret_number_position(self, dot: int) -> Tuple[float, float]: 120 | x = self.config.general.x_start + self.config.general.fret_width * ( 121 | 1 / 2 + dot - self.config.general.first_fret + 1 122 | ) 123 | y = self.config.general.y_start + self.config.general.fret_height * ( 124 | len(self.tuning) 125 | ) 126 | return x, y 127 | 128 | def get_tuning_position(self, string_no: int) -> Tuple[float, float]: 129 | x = self.config.general.x_start + ( 130 | self.config.general.fret_width 131 | * (self.config.general.last_fret - self.config.general.first_fret + 5 / 2) 132 | ) 133 | y = self.config.general.y_start + self.config.general.fret_height * (string_no) 134 | return x, y 135 | 136 | def get_single_note_position( 137 | self, string_no: int, index: int 138 | ) -> Tuple[float, float]: 139 | x_pos = index + (1 / 2) 140 | x = self.config.general.x_start + (self.config.general.fret_width) * x_pos 141 | y = self.config.general.y_start + self.config.general.fret_height * (string_no) 142 | return x, y 143 | 144 | def get_cross_position(self, string_no: int) -> Tuple[float, float]: 145 | x = self.config.general.x_start + self.config.general.fret_width * (1 / 2) 146 | y = self.config.general.y_start + (self.config.general.fret_height) * ( 147 | string_no 148 | ) 149 | return x, y 150 | 151 | def get_size(self) -> Tuple[float, float]: 152 | """Get total size of the drawing. 153 | 154 | Returns 155 | ------- 156 | Tuple[float, float] 157 | Width and heigth 158 | """ 159 | # We add 1 as it is one-indexed for first fret 160 | number_of_frets = ( 161 | self.config.general.last_fret - self.config.general.first_fret 162 | ) + 1 163 | width = ( 164 | self.config.general.x_start 165 | + self.config.general.fret_width * (number_of_frets + 2) 166 | + self.config.general.x_end_offset 167 | ) 168 | height = ( 169 | self.config.general.y_start 170 | + self.config.general.fret_height * (len(self.tuning) + 1) 171 | + self.config.general.y_end_offset 172 | ) 173 | return (width, height) 174 | 175 | def get_inside_bounds( 176 | self, 177 | ) -> Tuple[Tuple[float, float], Tuple[float, float]]: 178 | """Get the size of the inner drawing. 179 | 180 | This function could be use to add custom elements 181 | 182 | Returns 183 | ------- 184 | Tuple[Tuple[float, float], Tuple[float, float]] 185 | Upper left corner x coordinate, upper left corner y coordinate 186 | Lower right corner x coordinate, lower right corner y coordinate 187 | """ 188 | open_fret_width = self.config.general.fret_width 189 | # We add 1 as it is one-indexed for first fret 190 | number_of_frets = ( 191 | self.config.general.last_fret - self.config.general.first_fret 192 | ) + 1 193 | upper_left_x = self.config.general.x_start 194 | upper_left_y = self.config.general.y_start 195 | lower_right_x = ( 196 | self.config.general.x_start 197 | + open_fret_width 198 | + (number_of_frets) * (self.config.general.fret_width) 199 | ) 200 | lower_right_y = ( 201 | self.config.general.y_start 202 | + (len(self.tuning) - 1) * self.config.general.fret_height 203 | ) 204 | return ((upper_left_x, upper_left_y), (lower_right_x, lower_right_y)) 205 | -------------------------------------------------------------------------------- /fretboardgtr/notes_creators.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from itertools import product 3 | from typing import List, Optional 4 | 5 | from fretboardgtr.constants import CHORDS_DICT_ESSENTIAL, CHROMATICS_NOTES, SCALES_DICT 6 | from fretboardgtr.utils import chromatic_position_from_root, get_note_from_index 7 | 8 | 9 | def find_first_index(_list: List[int], value: int) -> Optional[int]: 10 | try: 11 | index = _list.index(value) 12 | return index 13 | except ValueError: 14 | return None # If the value is not found in the tuple, return None 15 | 16 | 17 | @dataclass 18 | class NotesContainer: 19 | root: str 20 | notes: List[str] 21 | 22 | def get_scale(self, tuning: List[str], max_spacing: int = 5) -> List[List[int]]: 23 | """Get the scale of each string in the given tuning. 24 | 25 | Goes from 0 up to (12 + max_spacing - 1) on the fretboard 26 | 27 | Parameters 28 | ---------- 29 | tuning (List[str]) 30 | The tuning of each string. 31 | 32 | Returns 33 | ------- 34 | List[List[int]] 35 | The scale of each string in the tuning. 36 | """ 37 | scale = [] 38 | 39 | # Iterate over each string in the tuning 40 | for string_note in tuning: 41 | indices = [] 42 | 43 | # Iterate over each note in the self.notes list 44 | for note in self.notes: 45 | _idx = chromatic_position_from_root(note, string_note) 46 | 47 | # Add the chromatic positions of the note on the string 48 | # until it reaches or exceeds the maximum position of 16 49 | while _idx <= 12 + max_spacing - 1: 50 | indices.append(_idx) 51 | _idx += 12 52 | 53 | # Sort the indices in ascending order 54 | scale.append(sorted(indices)) 55 | 56 | return scale 57 | 58 | def get_chord_fingerings( 59 | self, 60 | tuning: List[str], 61 | max_spacing: int = 5, 62 | min_notes_in_chord: int = 2, 63 | number_of_fingers: int = 4, 64 | ) -> List[List[Optional[int]]]: 65 | """Get all probably possible fingering for a specific tuning. 66 | 67 | Parameters 68 | ---------- 69 | tuning : List[str] 70 | List of note of the tuning 71 | max_spacing : int 72 | Maximum spacing between notes 73 | min_notes_in_chord : int 74 | Minimum number of notes in chord 75 | number_of_fingers : int 76 | Number of fingers allowed 77 | 78 | Returns 79 | ------- 80 | List[List[Optional[int]]] 81 | List of propably possible fingerings 82 | """ 83 | scale = self.get_scale(tuning, max_spacing) 84 | 85 | fingerings = [] 86 | for combination in product(*scale): 87 | non_zero_numbers = [num for num in combination if num != 0] 88 | # No more than 4 fingers but duplicated allowed 89 | if len(set(non_zero_numbers)) > number_of_fingers: 90 | continue 91 | 92 | new_combination = list(combination) 93 | while True: 94 | # If 0 note or only one this is not a chord so break 95 | if len(non_zero_numbers) < min_notes_in_chord: 96 | break 97 | # If the spacing is less than 5 then it'ok so break 98 | if max(non_zero_numbers) - min(non_zero_numbers) <= max_spacing: 99 | break 100 | 101 | # If the spacing is more than 5 then remplace min values by None 102 | index_of_min = find_first_index(new_combination, min(non_zero_numbers)) 103 | index_of_non_zero_min = find_first_index( 104 | non_zero_numbers, min(non_zero_numbers) 105 | ) 106 | if index_of_min is not None and index_of_non_zero_min is not None: 107 | new_combination[index_of_min] = None 108 | del non_zero_numbers[index_of_non_zero_min] 109 | 110 | notes = [] 111 | for index, note in zip(new_combination, tuning): 112 | if index is not None: 113 | notes.append(get_note_from_index(index, note)) 114 | 115 | # Each notes should appear at least once. 116 | if set(notes) != set(self.notes): 117 | continue 118 | 119 | fingerings.append(list(new_combination)) 120 | return fingerings 121 | 122 | def get_scale_positions( 123 | self, 124 | tuning: List[str], 125 | max_spacing: int = 5, 126 | ) -> List[List[List[Optional[int]]]]: 127 | """Get all possible scale positions for a specific tuning. 128 | 129 | Parameters 130 | ---------- 131 | tuning : List[str] 132 | List of note of the tuning 133 | max_spacing : int 134 | Maximum spacing between notes 135 | 136 | Returns 137 | ------- 138 | List[List[List[Optional[int]]]] 139 | List of all possible scale positions 140 | """ 141 | scale = self.get_scale(tuning, max_spacing) 142 | fingerings: List[List[List[Optional[int]]]] = [] 143 | for first_string_pos in scale[0]: 144 | fingering: List[List[Optional[int]]] = [] 145 | for string in scale: 146 | string_fingering: List[Optional[int]] = [] 147 | for note in string: 148 | if ( 149 | note - first_string_pos < 0 150 | or note - first_string_pos >= max_spacing 151 | ): 152 | continue 153 | string_fingering.append(note) 154 | fingering.append(string_fingering) 155 | fingerings.append(fingering) 156 | return fingerings 157 | 158 | 159 | class ScaleFromName: 160 | """Object that generating NotesContainer object from root and mode. 161 | 162 | Given a root name and a mode name, get the resulting scale. 163 | 164 | Also : 165 | Mode name can be given thanks to the constants.ModeName enum as well as string 166 | Note name can be given thanks to the constants.NoteName enum as well as string 167 | 168 | Example 169 | ------- 170 | >>> ScaleFromName(root='C',mode='Dorian').build() 171 | NotesContainer(root= 'C', scale = ['C', 'D', 'D#', 'F', 'G', 'A', 'A#']) 172 | >>> ScaleFromName(root=Name.C,mode=ModeName.DORIAN).build() 173 | NotesContainer(root= 'C', scale = ['C', 'D', 'D#', 'F', 'G', 'A', 'A#']) 174 | """ 175 | 176 | def __init__(self, root: str = "C", mode: str = "Ionian"): 177 | self.root = root 178 | self.mode = mode 179 | 180 | def build(self) -> NotesContainer: 181 | index = CHROMATICS_NOTES.index(self.root) 182 | mode_idx = SCALES_DICT[self.mode] 183 | scale = [] 184 | for note_id in mode_idx: 185 | scale.append(CHROMATICS_NOTES[(index + note_id) % 12]) 186 | return NotesContainer(self.root, scale) 187 | 188 | 189 | class ChordFromName: 190 | """Object generating NotesContainer object from root and chord quality. 191 | 192 | Given a root name and a quality name, get the resulting scale. 193 | 194 | Also : 195 | Mode name can be given thanks to the constants.ChordName enum as well as string 196 | Note name can be given thanks to the constants.NoteName enum as well as string 197 | 198 | Example 199 | ------- 200 | >>> ChordFromName(root='C',quality='M').build() 201 | NotesContainer(root= 'C', scale = ['C', 'E', 'G']) 202 | >>> ChordFromName(root=NoteName.C,quality=ChordName.MAJOR).build() 203 | NotesContainer(root= 'C', scale = ['C', 'E', 'G']) 204 | """ 205 | 206 | def __init__(self, root: str = "C", quality: str = "M"): 207 | self.root = root 208 | self.quality = quality 209 | 210 | def build(self) -> NotesContainer: 211 | index = CHROMATICS_NOTES.index(self.root) 212 | 213 | quality_idx = CHORDS_DICT_ESSENTIAL[self.quality] 214 | scale = [] 215 | for note_id in quality_idx: 216 | scale.append(CHROMATICS_NOTES[(index + note_id) % 12]) 217 | return NotesContainer(self.root, scale) 218 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - "docs/**" 7 | - "*.md" 8 | - "*.rst" 9 | 10 | pull_request: 11 | paths-ignore: 12 | - "docs/**" 13 | - "*.md" 14 | - "*.rst" 15 | 16 | jobs: 17 | pre-commit: 18 | name: Run pre-commit 19 | # This is the VM in which github run 20 | # We can use a custom container that will 21 | # be run in this VM 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Check out the repository 25 | uses: actions/checkout@v3 26 | with: 27 | lfs: true 28 | 29 | - name: Set up Python 30 | uses: actions/setup-python@v4 31 | with: 32 | python-version: "3.11" 33 | 34 | - name: Cache python environment 35 | uses: actions/cache@v3 36 | # https://tobiasmcnulty.com/posts/caching-pre-commit/ 37 | # This restore and/or save in the same action 38 | with: 39 | path: ~/.cache/pip 40 | key: ${{ runner.os }}-python-3.11-venv-${{ hashFiles('pyproject.toml') }} 41 | 42 | - name: Install dependencies 43 | # Activate or create env from cache and install package 44 | # https://stackoverflow.com/questions/74668349/how-to-activate-a-virtualenv-in-a-github-action 45 | # Note that you have to activate the virtualenv in every step 46 | # because GitHub actions doesn't preserve the environment 47 | run: | 48 | pip install -U '.[dev]' 49 | 50 | - name: Cache pre-commits 51 | id: cache-pre-commits 52 | # https://tobiasmcnulty.com/posts/caching-pre-commit/ 53 | # This restore and/or save in the same action 54 | uses: actions/cache@v3 55 | with: 56 | path: ~/.cache/pre-commit/ 57 | key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} 58 | 59 | - name: Install pre-commits 60 | # https://github.com/actions/cache#restoring-and-saving-cache-using-a-single-action 61 | if: steps.cache-pre-commits.outputs.cache-hit != 'true' 62 | run: pre-commit install 63 | 64 | - name: Compute pre-commit cache key 65 | run: | 66 | pre-commit run --all-files 67 | 68 | multi-os-tests: 69 | name: Test for Python ${{ matrix.python }} on ${{ matrix.os }} 70 | runs-on: ${{ matrix.os }} 71 | # Only test all OS on default branch 72 | if: ( github.ref == format('refs/heads/{0}', github.event.repository.default_branch) ) || startsWith(github.ref, 'refs/tags') 73 | needs: pre-commit 74 | strategy: 75 | fail-fast: false 76 | # https://github.com/actions/cache/blob/main/examples.md#multiple-oss-in-a-workflow-with-a-matrix 77 | matrix: 78 | include: 79 | - { 80 | python: "3.10", 81 | os: "windows-latest", 82 | session: "tests", 83 | cache-path: ~\AppData\Local\pip\Cache, 84 | } 85 | - { 86 | python: "3.10", 87 | os: "macos-latest", 88 | session: "tests", 89 | cache-path: ~/Library/Caches/pip, 90 | } 91 | - { 92 | python: "3.9", 93 | os: "ubuntu-latest", 94 | session: "tests", 95 | cache-path: ~/.cache/pip, 96 | } 97 | - { 98 | python: "3.10", 99 | os: "ubuntu-latest", 100 | session: "tests", 101 | cache-path: ~/.cache/pip, 102 | } 103 | env: 104 | FORCE_COLOR: "1" 105 | PRE_COMMIT_COLOR: "always" 106 | 107 | steps: 108 | - name: Check out the repository 109 | uses: actions/checkout@v3 110 | with: 111 | lfs: true 112 | 113 | - name: Set up Python ${{ matrix.python }} 114 | uses: actions/setup-python@v4 115 | with: 116 | python-version: ${{ matrix.python }} 117 | 118 | - name: Cache python environment 119 | uses: actions/cache@v3 120 | with: 121 | path: ${{ matrix.cache-path }} 122 | key: ${{ runner.os }}-python-${{ matrix.python }}-pip-${{ hashFiles('pyproject.toml') }} 123 | restore-keys: | 124 | ${{ runner.os }}-pip- 125 | 126 | - name: Install dependencies 127 | # Activate or create env from cache and install package 128 | run: | 129 | pip install -U '.[dev]' 130 | 131 | - name: Run tests 132 | run: | 133 | python -m pytest 134 | 135 | linux-tests: 136 | name: Tests for Python 3.11 on ubuntu-latest 137 | runs-on: ubuntu-latest 138 | needs: pre-commit 139 | # If not default branch 140 | # It allows us to not use too much walltime of github 141 | env: 142 | FORCE_COLOR: "1" 143 | PRE_COMMIT_COLOR: "always" 144 | 145 | steps: 146 | - name: Check out the repository 147 | uses: actions/checkout@v3 148 | with: 149 | lfs: true 150 | 151 | - name: Set up Python 3.11 152 | uses: actions/setup-python@v4 153 | with: 154 | python-version: "3.11" 155 | 156 | - name: Cache python environment 157 | uses: actions/cache@v3 158 | # https://tobiasmcnulty.com/posts/caching-pre-commit/ 159 | # This restore and/or save in the same action 160 | with: 161 | path: ~/.cache/pip 162 | key: ${{ runner.os }}-python-3.11-pip-${{ hashFiles('pyproject.toml') }} 163 | 164 | - name: Install dependencies 165 | # Activate or create env from cache and install package 166 | run: | 167 | pip install -U '.[dev]' 168 | 169 | - name: Run tests 170 | run: | 171 | python -m pytest 172 | 173 | - name: Upload artifacts generated by tests 174 | uses: actions/upload-artifact@v3 175 | with: 176 | name: tests-artifacts 177 | path: tests/data/outputs/ 178 | 179 | - name: Upload coverage data 180 | uses: "actions/upload-artifact@v3" 181 | with: 182 | name: coverage-data 183 | path: "./coverage/" 184 | 185 | coverage: 186 | name: Coverage deployment 187 | runs-on: ubuntu-latest 188 | needs: linux-tests 189 | # Only deploy coverage on default branch 190 | if: ( github.ref == format('refs/heads/{0}', github.event.repository.default_branch) ) || startsWith(github.ref, 'refs/tags') 191 | steps: 192 | - name: Check out the repository 193 | uses: actions/checkout@v3 194 | with: 195 | lfs: true 196 | 197 | - name: Download coverage data 198 | uses: actions/download-artifact@v3 199 | with: 200 | name: coverage-data 201 | path: ./coverage/ 202 | 203 | - name: Display structure of downloaded files 204 | run: ls -lh --color=auto -R 205 | 206 | - name: Upload coverage report 207 | uses: codecov/codecov-action@v3 208 | with: 209 | token: ${{ secrets.CODECOV_TOKEN }} 210 | directory: ./coverage/ 211 | fail_ci_if_error: true 212 | files: coverage.xml 213 | verbose: true 214 | 215 | test-pypi: 216 | name: Deploy Package to Test PyPI 217 | runs-on: ubuntu-latest 218 | needs: [pre-commit] 219 | steps: 220 | - name: Checkout 221 | uses: actions/checkout@v3 222 | with: 223 | lfs: true 224 | fetch-depth: 0 225 | 226 | - name: Set up Python 227 | uses: actions/setup-python@v4 228 | with: 229 | python-version: "3.10" 230 | 231 | - name: Install pypa/build 232 | run: >- 233 | python -m 234 | pip install 235 | --upgrade 236 | build 237 | --user 238 | 239 | - name: Build a binary wheel and a source tarball 240 | run: >- 241 | python -m 242 | build 243 | --sdist 244 | --wheel 245 | --outdir dist/ 246 | . 247 | 248 | - name: Publish package to TestPyPI 249 | uses: pypa/gh-action-pypi-publish@release/v1 250 | with: 251 | password: ${{ secrets.TEST_PYPI_TOKEN }} 252 | repository_url: https://test.pypi.org/legacy/ 253 | skip_existing: true 254 | 255 | pypi: 256 | name: Deploy Package to PyPI 257 | # Deploy only tagged commit and if tests have succeeded 258 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 259 | needs: [linux-tests, multi-os-tests] 260 | runs-on: ubuntu-latest 261 | steps: 262 | - name: Checkout 263 | uses: actions/checkout@v3 264 | with: 265 | lfs: true 266 | fetch-depth: 0 267 | 268 | - name: Set up Python 269 | uses: actions/setup-python@v4 270 | with: 271 | python-version: "3.11" 272 | 273 | - name: Install pypa/build 274 | run: >- 275 | python -m 276 | pip install 277 | --upgrade 278 | build 279 | --user 280 | 281 | - name: Build a binary wheel and a source tarball 282 | run: >- 283 | python -m 284 | build 285 | --sdist 286 | --wheel 287 | --outdir dist/ 288 | . 289 | 290 | - name: Publish package to PyPI 291 | uses: pypa/gh-action-pypi-publish@release/v1 292 | with: 293 | password: ${{ secrets.PYPI_TOKEN }} 294 | --------------------------------------------------------------------------------