Skip to content

Commit

Permalink
feat: Add scale positions generator
Browse files Browse the repository at this point in the history
  • Loading branch information
antscloud committed Nov 15, 2023
1 parent 83dbb3b commit 5c47b41
Show file tree
Hide file tree
Showing 3 changed files with 176 additions and 55 deletions.
99 changes: 70 additions & 29 deletions docs/source/get-started/get-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ To get started simply install the package from PyPI

## How to install

`fretboardgtr` needs to have the following install in order to run :

```shell
sudo apt install libcairo2-dev pkg-config
```

```shell
pip install fretboardgtr
```
Expand Down Expand Up @@ -111,34 +117,69 @@ fretboard.export("my_vertical_fretboard.svg", format="svg")

⚠️ Be careful with this snippets. This example generates over 1000 svgs
```python
TUNING = ["E", "A", "D", "G", "B", "E"]
CHORD_ROOT = "C"
CHORD_QUALITY = "M"

fingerings = (
ChordFromName(root=CHORD_ROOT, quality=CHORD_QUALITY)
.get()
.get_probablely_possible_fingering(TUNING)
)
for i, fingering in enumerate(fingerings):
_cleaned_fingering = [pos for pos in fingering if pos is not None and pos != 0]
first_fret = min(_cleaned_fingering) - 2
if first_fret < 0:
first_fret = 0

last_fret = max(_cleaned_fingering) + 2
if last_fret < 4:
last_fret = 4

config = {
"general": {
"first_fret": first_fret,
"last_fret": last_fret,
"fret_width": 50,
}
from fretboardgtr.fretboard import FretBoardConfig, FretBoard
from fretboardgtr.constants import Chord
from fretboardgtr.notes_creators import ChordFromName

TUNING = ["E", "A", "D", "G", "B", "E"]
ROOT = "C"
QUALITY = Chord.MAJOR

fingerings = (
ChordFromName(root=ROOT, quality=QUALITY).get().get_chord_fingerings(TUNING)
)
for i, fingering in enumerate(fingerings):
_cleaned_fingering = [pos for pos in fingering if pos is not None and pos != 0]
first_fret = min(_cleaned_fingering) - 2
if first_fret < 0:
first_fret = 0

last_fret = max(_cleaned_fingering) + 2
if last_fret < 4:
last_fret = 4

config = {
"general": {
"first_fret": first_fret,
"last_fret": last_fret,
"fret_width": 50,
}
fretboard_config = FretBoardConfig.from_dict(config)
fretboard = FretBoard(config=fretboard_config, tuning=TUNING, vertical=True)
fretboard.add_fingering(fingering, root=CHORD_ROOT)
fretboard.export(f"./C_Major/C_Major_position_{i}.svg", format="svg")
}
fretboard_config = FretBoardConfig.from_dict(config)
fretboard = FretBoard(config=fretboard_config, tuning=TUNING, vertical=True)
fretboard.add_fingering(fingering, root=ROOT)
fretboard.export(
f"./{ROOT}_{QUALITY.value}/{ROOT}_{QUALITY.value}_position_{i}.svg",
format="svg",
)


```

### Generate all the classic positions for A minor pentatonic scale

```python
from fretboardgtr.fretboard import FretBoardConfig, FretBoard
from fretboardgtr.notes_creators import ScaleFromName
from fretboardgtr.constants import Mode

TUNING = ["E", "A", "D", "G", "B", "E"]
ROOT = "A"
MODE = Mode.MINOR_PENTATONIC

scale_positions = (
ScaleFromName(root=ROOT, mode=MODE).get().get_scale_positions(TUNING, max_spacing=4)
)
config = {
"general": {
"last_fret": 16,
}
}

for i, scale_position in enumerate(scale_positions):
fretboard = FretBoard(config=config, tuning=TUNING)
fretboard.add_scale(scale_position, root=ROOT)
fretboard.export(
f"./{ROOT}_{MODE.value}/{ROOT}_{MODE.value}_position_{i}.svg", format="svg"
)
```
121 changes: 96 additions & 25 deletions fretboardgtr/notes_creators.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,59 +19,93 @@ class NotesContainer:
root: str
notes: List[str]

def get_probablely_possible_fingering(
self, tuning: List[str]
) -> List[List[Optional[int]]]:
"""Get all probably possible fingering for a specific tuning.
def _get_scale(self, tuning: List[str], max_spacing: int = 5) -> List[List[int]]:
"""Get the scale of each string in the given tuning.
Goes from 0 up to (12 + max_spacing - 1) on the fretboard
Parameters
----------
tuning : List[str]
List of note of the tuning
tuning (List[str])
The tuning of each string.
Returns
-------
List[List[Optional[int]]]
List of propably possible fingerings
List[List[int]]
The scale of each string in the tuning.
"""
scale = []

# Iterate over each string in the tuning
for string_note in tuning:
indices = []

# Iterate over each note in the self.notes list
for note in self.notes:
_idx = chromatic_position_from_root(note, string_note)
while _idx <= 16:

# Add the chromatic positions of the note on the string
# until it reaches or exceeds the maximum position of 16
while _idx <= 12 + max_spacing - 1:
indices.append(_idx)
_idx += 12

# Sort the indices in ascending order
scale.append(sorted(indices))

return scale

def get_chord_fingerings(
self,
tuning: List[str],
max_spacing: int = 5,
min_notes_in_chord: int = 2,
number_of_fingers: int = 4,
) -> List[List[Optional[int]]]:
"""Get all probably possible fingering for a specific tuning.
Parameters
----------
tuning : List[str]
List of note of the tuning
max_spacing : int
Maximum spacing between notes
min_notes_in_chord : int
Minimum number of notes in chord
number_of_fingers : int
Number of fingers allowed
Returns
-------
List[List[Optional[int]]]
List of propably possible fingerings
"""
scale = self._get_scale(tuning, max_spacing)

fingerings = []
for combination in product(*scale):
non_zero_numbers = [num for num in combination if num != 0]

# No more than 4 fingers but duplicated allowed
if len(set(non_zero_numbers)) > 4:
if len(set(non_zero_numbers)) > number_of_fingers:
continue

# No more than 5 frets spacing
# else try to remplace min values by None
# TODO: Also add the max checking
new_combination = list(combination)

while True:
# If 0 note or only one this is not a chord so break
if len(non_zero_numbers) < min_notes_in_chord:
break
# If the spacing is less than 5 then it'ok so break
if max(non_zero_numbers) - min(non_zero_numbers) <= max_spacing:
break

# If the spacing is more than 5 then remplace min values by None
index_of_min = find_first_index(new_combination, min(non_zero_numbers))
index_of_min_of_non_zero = find_first_index(
index_of_non_zero_min = find_first_index(
non_zero_numbers, min(non_zero_numbers)
)

if index_of_min is not None and index_of_min_of_non_zero is not None:
if index_of_min is not None and index_of_non_zero_min is not None:
new_combination[index_of_min] = None
del non_zero_numbers[index_of_min_of_non_zero]

if len(non_zero_numbers) < 2:
break

if max(non_zero_numbers) - min(non_zero_numbers) <= 5:
break
del non_zero_numbers[index_of_non_zero_min]

notes = []
for index, note in zip(new_combination, tuning):
Expand All @@ -81,9 +115,46 @@ def get_probablely_possible_fingering(
# Each notes should appear at least once.
if set(notes) != set(self.notes):
continue

fingerings.append(list(new_combination))
return fingerings

def get_scale_positions(
self,
tuning: List[str],
max_spacing: int = 5,
) -> List[List[List[Optional[int]]]]:
"""Get all possible scale positions for a specific tuning.
Parameters
----------
tuning : List[str]
List of note of the tuning
max_spacing : int
Maximum spacing between notes
Returns
-------
List[List[List[Optional[int]]]]
List of all possible scale positions
"""
scale = self._get_scale(tuning, max_spacing)
fingerings: List[List[List[Optional[int]]]] = []
for first_string_pos in scale[0]:
fingering: List[List[Optional[int]]] = []
for string in scale:
string_fingering: List[Optional[int]] = []
for note in string:
if (
note - first_string_pos < 0
or note - first_string_pos >= max_spacing
):
continue
string_fingering.append(note)
fingering.append(string_fingering)
fingerings.append(fingering)
return fingerings


class ScaleFromName:
"""Object that generating NotesContainer object from root and mode.
Expand Down
11 changes: 10 additions & 1 deletion tests/test_notes_creator.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ def test_chord_creator_fingerings():
fingerings = (
ChordFromName(root="C", quality="M")
.get()
.get_probablely_possible_fingering(["E", "A", "D", "G", "B", "E"])
.get_chord_fingerings(["E", "A", "D", "G", "B", "E"])
)
assert len(fingerings) > 1000


def test_scale_creator_position():
scale_positions = (
ScaleFromName(root="C", mode="Ionian")
.get()
.get_scale_positions(["E", "A", "D", "G", "B", "E"])
)
assert len(scale_positions) > 5

0 comments on commit 5c47b41

Please sign in to comment.