concertina_helper.layouts.bisonoric

  1from __future__ import annotations
  2from typing import Any
  3from collections.abc import Callable
  4from dataclasses import dataclass
  5
  6from .unisonoric import UnisonoricFingering, UnisonoricLayout
  7from ..type_defs import Shape, PitchToStr, Mask, Pitch, Direction, Annotation
  8from .base_classes import Layout, Fingering
  9
 10
 11@dataclass(frozen=True, kw_only=True)
 12class BisonoricLayout(Layout['BisonoricFingering']):
 13    '''
 14    Represents a bisonoric concertina layout:
 15    the layout of the buttons on the left and right,
 16    and the pitches they produce on push and pull.
 17
 18    >>> from concertina_helper.layouts.layout_loader import (
 19    ...     load_bisonoric_layout_by_name)
 20    >>> layout = load_bisonoric_layout_by_name('30_wheatstone_cg')
 21    >>> print(layout)
 22    PUSH:
 23    E3  A3  C#4 A4  G#4     C#5 A5  G#5 C#6 A6
 24    C3  G3  C4  E4  G4      C5  E5  G5  C6  E6
 25    B3  D4  G4  B4  D5      G5  B5  D6  G6  B6
 26    PULL:
 27    F3  Bb3 D#4 G4  Bb4     D#5 G5  Bb5 D#6 F6
 28    G3  B3  D4  F4  A4      B4  D5  F5  A5  B5
 29    A3  F#4 A4  C5  E5      F#5 A5  C6  E6  F#6
 30
 31    With a layout, you can get all fingerings for a particular pitch.
 32    Fingerings can be combined to produce chords:
 33
 34    >>> c = layout.get_fingerings(Pitch('C4')).pop()
 35    >>> e = layout.get_fingerings(Pitch('E4')).pop()
 36    >>> print(c | e)
 37    PUSH:
 38    --- --- --- --- ---    --- --- --- --- ---
 39    --- --- C4  E4  ---    --- --- --- --- ---
 40    --- --- --- --- ---    --- --- --- --- ---
 41
 42    Fingerings with different bellow directions can not be combined:
 43    >>> f = layout.get_fingerings(Pitch('F4')).pop()
 44    >>> print(c | f)
 45    Traceback (most recent call last):
 46    ...
 47    ValueError: different bellows directions
 48    '''
 49    push_layout: UnisonoricLayout
 50    pull_layout: UnisonoricLayout
 51
 52    def __post_init__(self) -> None:
 53        if self.push_layout.shape != self.pull_layout.shape:
 54            raise ValueError(
 55                'Push and pull layout shapes must match: '
 56                f'{self.push_layout.shape} != {self.pull_layout.shape}')
 57
 58    @property
 59    def shape(self) -> Shape:
 60        return (
 61            [len(row) for row in self.push_layout.left],
 62            [len(row) for row in self.push_layout.right],
 63        )
 64
 65    def get_fingerings(self, pitch: Pitch) -> set[BisonoricFingering]:
 66        '''
 67        Given a pitch, return all possible fingerings as a set.
 68        '''
 69        push_fingerings = self.push_layout.get_fingerings(pitch)
 70        pull_fingerings = self.pull_layout.get_fingerings(pitch)
 71        return (
 72            {BisonoricFingering(Direction.PUSH, pf) for pf in push_fingerings} |
 73            {BisonoricFingering(Direction.PULL, pf) for pf in pull_fingerings}
 74        )
 75
 76    def __str__(self) -> str:
 77        return f'{Direction.PUSH.name}:\n{self.push_layout}\n' \
 78            f'{Direction.PULL.name}:\n{self.pull_layout}'
 79
 80    def transpose(self, semitones: int) -> BisonoricLayout:
 81        '''
 82        Given a number of semitones, return a new layout,
 83        transposed up or down.
 84        '''
 85        return BisonoricLayout(
 86            push_layout=self.push_layout.transpose(semitones),
 87            pull_layout=self.pull_layout.transpose(semitones))
 88
 89
 90@dataclass(frozen=True)
 91class BisonoricFingering(Fingering):
 92    '''
 93    Represents a fingering on a bisonoric concertina.
 94    '''
 95    direction: Direction
 96    _fingering: UnisonoricFingering
 97
 98    @property
 99    def left_mask(self) -> Mask:
100        return self._fingering.left_mask
101
102    @property
103    def right_mask(self) -> Mask:
104        return self._fingering.right_mask
105
106    def __str__(self) -> str:
107        return f'{self.direction.name}:\n{self._fingering}'
108
109    def format(
110        self,
111        button_down_f: PitchToStr = lambda pitch: '@',
112        button_up_f: PitchToStr = lambda pitch: '.',
113        direction_f: Callable[[Direction], str] =
114            lambda direction: direction.name) -> str:
115        return f'{direction_f(self.direction)}:\n' \
116            f'{self._fingering.format(button_down_f, button_up_f)}'
117
118    def __or__(self, other: Any) -> BisonoricFingering:
119        if type(self) != type(other):
120            raise TypeError('mixed operand types')
121        if self.direction != other.direction:
122            raise ValueError('different bellows directions')
123        return BisonoricFingering(self.direction, self._fingering | other._fingering)
124
125    def get_pitches(self) -> set[Pitch]:
126        return self._fingering.get_pitches()
127
128
129@dataclass(frozen=True, kw_only=True)
130class AnnotatedBisonoricFingering:
131    '''
132    Adds contextual information to the fingering
133    that is useful in finding the best fingering for a tune.
134    '''
135    fingering: BisonoricFingering
136    annotation: Annotation
137
138    def __str__(self) -> str:
139        a = self.annotation
140        return f'Measure {a.measure} - {a.pitch}\n{self.fingering}'
141
142    def format(  # pragma: no branch
143            self,
144            button_down_f: PitchToStr = lambda pitch: '@',
145            button_up_f: PitchToStr = lambda pitch: '.',
146            direction_f: Callable[[Direction], str] =
147            lambda direction: direction.name) -> str:
148        a = self.annotation
149        formatted = self.fingering.format(
150            button_down_f=button_down_f,
151            button_up_f=button_up_f,
152            direction_f=direction_f)
153        return f'Measure {a.measure} - {a.pitch}\n{formatted}'
@dataclass(frozen=True, kw_only=True)
class BisonoricLayout(concertina_helper.layouts.base_classes.Layout[ForwardRef('BisonoricFingering')]):
12@dataclass(frozen=True, kw_only=True)
13class BisonoricLayout(Layout['BisonoricFingering']):
14    '''
15    Represents a bisonoric concertina layout:
16    the layout of the buttons on the left and right,
17    and the pitches they produce on push and pull.
18
19    >>> from concertina_helper.layouts.layout_loader import (
20    ...     load_bisonoric_layout_by_name)
21    >>> layout = load_bisonoric_layout_by_name('30_wheatstone_cg')
22    >>> print(layout)
23    PUSH:
24    E3  A3  C#4 A4  G#4     C#5 A5  G#5 C#6 A6
25    C3  G3  C4  E4  G4      C5  E5  G5  C6  E6
26    B3  D4  G4  B4  D5      G5  B5  D6  G6  B6
27    PULL:
28    F3  Bb3 D#4 G4  Bb4     D#5 G5  Bb5 D#6 F6
29    G3  B3  D4  F4  A4      B4  D5  F5  A5  B5
30    A3  F#4 A4  C5  E5      F#5 A5  C6  E6  F#6
31
32    With a layout, you can get all fingerings for a particular pitch.
33    Fingerings can be combined to produce chords:
34
35    >>> c = layout.get_fingerings(Pitch('C4')).pop()
36    >>> e = layout.get_fingerings(Pitch('E4')).pop()
37    >>> print(c | e)
38    PUSH:
39    --- --- --- --- ---    --- --- --- --- ---
40    --- --- C4  E4  ---    --- --- --- --- ---
41    --- --- --- --- ---    --- --- --- --- ---
42
43    Fingerings with different bellow directions can not be combined:
44    >>> f = layout.get_fingerings(Pitch('F4')).pop()
45    >>> print(c | f)
46    Traceback (most recent call last):
47    ...
48    ValueError: different bellows directions
49    '''
50    push_layout: UnisonoricLayout
51    pull_layout: UnisonoricLayout
52
53    def __post_init__(self) -> None:
54        if self.push_layout.shape != self.pull_layout.shape:
55            raise ValueError(
56                'Push and pull layout shapes must match: '
57                f'{self.push_layout.shape} != {self.pull_layout.shape}')
58
59    @property
60    def shape(self) -> Shape:
61        return (
62            [len(row) for row in self.push_layout.left],
63            [len(row) for row in self.push_layout.right],
64        )
65
66    def get_fingerings(self, pitch: Pitch) -> set[BisonoricFingering]:
67        '''
68        Given a pitch, return all possible fingerings as a set.
69        '''
70        push_fingerings = self.push_layout.get_fingerings(pitch)
71        pull_fingerings = self.pull_layout.get_fingerings(pitch)
72        return (
73            {BisonoricFingering(Direction.PUSH, pf) for pf in push_fingerings} |
74            {BisonoricFingering(Direction.PULL, pf) for pf in pull_fingerings}
75        )
76
77    def __str__(self) -> str:
78        return f'{Direction.PUSH.name}:\n{self.push_layout}\n' \
79            f'{Direction.PULL.name}:\n{self.pull_layout}'
80
81    def transpose(self, semitones: int) -> BisonoricLayout:
82        '''
83        Given a number of semitones, return a new layout,
84        transposed up or down.
85        '''
86        return BisonoricLayout(
87            push_layout=self.push_layout.transpose(semitones),
88            pull_layout=self.pull_layout.transpose(semitones))

Represents a bisonoric concertina layout: the layout of the buttons on the left and right, and the pitches they produce on push and pull.

>>> from concertina_helper.layouts.layout_loader import (
...     load_bisonoric_layout_by_name)
>>> layout = load_bisonoric_layout_by_name('30_wheatstone_cg')
>>> print(layout)
PUSH:
E3  A3  C#4 A4  G#4     C#5 A5  G#5 C#6 A6
C3  G3  C4  E4  G4      C5  E5  G5  C6  E6
B3  D4  G4  B4  D5      G5  B5  D6  G6  B6
PULL:
F3  Bb3 D#4 G4  Bb4     D#5 G5  Bb5 D#6 F6
G3  B3  D4  F4  A4      B4  D5  F5  A5  B5
A3  F#4 A4  C5  E5      F#5 A5  C6  E6  F#6

With a layout, you can get all fingerings for a particular pitch. Fingerings can be combined to produce chords:

>>> c = layout.get_fingerings(Pitch('C4')).pop()
>>> e = layout.get_fingerings(Pitch('E4')).pop()
>>> print(c | e)
PUSH:
--- --- --- --- ---    --- --- --- --- ---
--- --- C4  E4  ---    --- --- --- --- ---
--- --- --- --- ---    --- --- --- --- ---

Fingerings with different bellow directions can not be combined:

>>> f = layout.get_fingerings(Pitch('F4')).pop()
>>> print(c | f)
Traceback (most recent call last):
...
ValueError: different bellows directions
shape: tuple[typing.Iterable[int], typing.Iterable[int]]

Returns tuple representing the number of buttons in each row, left and right.

def get_fingerings( self, pitch: concertina_helper.type_defs.Pitch) -> set[concertina_helper.layouts.bisonoric.BisonoricFingering]:
66    def get_fingerings(self, pitch: Pitch) -> set[BisonoricFingering]:
67        '''
68        Given a pitch, return all possible fingerings as a set.
69        '''
70        push_fingerings = self.push_layout.get_fingerings(pitch)
71        pull_fingerings = self.pull_layout.get_fingerings(pitch)
72        return (
73            {BisonoricFingering(Direction.PUSH, pf) for pf in push_fingerings} |
74            {BisonoricFingering(Direction.PULL, pf) for pf in pull_fingerings}
75        )

Given a pitch, return all possible fingerings as a set.

def transpose( self, semitones: int) -> concertina_helper.layouts.bisonoric.BisonoricLayout:
81    def transpose(self, semitones: int) -> BisonoricLayout:
82        '''
83        Given a number of semitones, return a new layout,
84        transposed up or down.
85        '''
86        return BisonoricLayout(
87            push_layout=self.push_layout.transpose(semitones),
88            pull_layout=self.pull_layout.transpose(semitones))

Given a number of semitones, return a new layout, transposed up or down.

@dataclass(frozen=True)
class BisonoricFingering(concertina_helper.layouts.base_classes.Fingering):
 91@dataclass(frozen=True)
 92class BisonoricFingering(Fingering):
 93    '''
 94    Represents a fingering on a bisonoric concertina.
 95    '''
 96    direction: Direction
 97    _fingering: UnisonoricFingering
 98
 99    @property
100    def left_mask(self) -> Mask:
101        return self._fingering.left_mask
102
103    @property
104    def right_mask(self) -> Mask:
105        return self._fingering.right_mask
106
107    def __str__(self) -> str:
108        return f'{self.direction.name}:\n{self._fingering}'
109
110    def format(
111        self,
112        button_down_f: PitchToStr = lambda pitch: '@',
113        button_up_f: PitchToStr = lambda pitch: '.',
114        direction_f: Callable[[Direction], str] =
115            lambda direction: direction.name) -> str:
116        return f'{direction_f(self.direction)}:\n' \
117            f'{self._fingering.format(button_down_f, button_up_f)}'
118
119    def __or__(self, other: Any) -> BisonoricFingering:
120        if type(self) != type(other):
121            raise TypeError('mixed operand types')
122        if self.direction != other.direction:
123            raise ValueError('different bellows directions')
124        return BisonoricFingering(self.direction, self._fingering | other._fingering)
125
126    def get_pitches(self) -> set[Pitch]:
127        return self._fingering.get_pitches()

Represents a fingering on a bisonoric concertina.

def format( self, button_down_f: collections.abc.Callable[[concertina_helper.type_defs.Pitch], str] = <function BisonoricFingering.<lambda>>, button_up_f: collections.abc.Callable[[concertina_helper.type_defs.Pitch], str] = <function BisonoricFingering.<lambda>>, direction_f: collections.abc.Callable[[concertina_helper.type_defs.Direction], str] = <function BisonoricFingering.<lambda>>) -> str:
110    def format(
111        self,
112        button_down_f: PitchToStr = lambda pitch: '@',
113        button_up_f: PitchToStr = lambda pitch: '.',
114        direction_f: Callable[[Direction], str] =
115            lambda direction: direction.name) -> str:
116        return f'{direction_f(self.direction)}:\n' \
117            f'{self._fingering.format(button_down_f, button_up_f)}'

Returns a formatted, human-readable string

def get_pitches(self) -> set[concertina_helper.type_defs.Pitch]:
126    def get_pitches(self) -> set[Pitch]:
127        return self._fingering.get_pitches()

Returns the pitches that would be produced by this fingering

@dataclass(frozen=True, kw_only=True)
class AnnotatedBisonoricFingering:
130@dataclass(frozen=True, kw_only=True)
131class AnnotatedBisonoricFingering:
132    '''
133    Adds contextual information to the fingering
134    that is useful in finding the best fingering for a tune.
135    '''
136    fingering: BisonoricFingering
137    annotation: Annotation
138
139    def __str__(self) -> str:
140        a = self.annotation
141        return f'Measure {a.measure} - {a.pitch}\n{self.fingering}'
142
143    def format(  # pragma: no branch
144            self,
145            button_down_f: PitchToStr = lambda pitch: '@',
146            button_up_f: PitchToStr = lambda pitch: '.',
147            direction_f: Callable[[Direction], str] =
148            lambda direction: direction.name) -> str:
149        a = self.annotation
150        formatted = self.fingering.format(
151            button_down_f=button_down_f,
152            button_up_f=button_up_f,
153            direction_f=direction_f)
154        return f'Measure {a.measure} - {a.pitch}\n{formatted}'

Adds contextual information to the fingering that is useful in finding the best fingering for a tune.

def format( self, button_down_f: collections.abc.Callable[[concertina_helper.type_defs.Pitch], str] = <function AnnotatedBisonoricFingering.<lambda>>, button_up_f: collections.abc.Callable[[concertina_helper.type_defs.Pitch], str] = <function AnnotatedBisonoricFingering.<lambda>>, direction_f: collections.abc.Callable[[concertina_helper.type_defs.Direction], str] = <function AnnotatedBisonoricFingering.<lambda>>) -> str:
143    def format(  # pragma: no branch
144            self,
145            button_down_f: PitchToStr = lambda pitch: '@',
146            button_up_f: PitchToStr = lambda pitch: '.',
147            direction_f: Callable[[Direction], str] =
148            lambda direction: direction.name) -> str:
149        a = self.annotation
150        formatted = self.fingering.format(
151            button_down_f=button_down_f,
152            button_up_f=button_up_f,
153            direction_f=direction_f)
154        return f'Measure {a.measure} - {a.pitch}\n{formatted}'