├── 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 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
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 | 
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 |
--------------------------------------------------------------------------------
/docs/source/assets/C_M/C_M_position_0.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/source/assets/C_M/C_M_position_1.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/source/assets/C_M/C_M_position_3.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/docs/source/assets/A_Minorpentatonic/A_Minorpentatonic_position_6.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/docs/source/assets/A_Minorpentatonic/A_Minorpentatonic_position_1.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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 | 
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 | 
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/docs/source/assets/A_Minorpentatonic/A_Minorpentatonic_position_2.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/source/assets/A_Minorpentatonic/A_Minorpentatonic_position_5.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------