concertina_helper.layouts.unisonoric

  1from __future__ import annotations
  2from dataclasses import dataclass
  3from typing import Any
  4from collections.abc import Callable
  5
  6from ..type_defs import Shape, Pitch, PitchToStr, PitchMatrix, Mask, Direction
  7from .base_classes import Layout, Fingering
  8
  9
 10@dataclass(frozen=True)
 11class UnisonoricLayout(Layout['UnisonoricFingering']):
 12    left: PitchMatrix
 13    right: PitchMatrix
 14
 15    @property
 16    def shape(self) -> Shape:
 17        return (
 18            [len(row) for row in self.left],
 19            [len(row) for row in self.right],
 20        )
 21
 22    def __make_masks(self, pitch: Pitch, pm: PitchMatrix) -> set[Mask]:
 23        masks = set()
 24        for i, row in enumerate(pm):
 25            for j, button in enumerate(row):
 26                if pitch == button:
 27                    mutable_mask = [[False] * len(row) for row in pm]
 28                    mutable_mask[i][j] = True
 29                    mask = Mask(tuple(
 30                        tuple(row) for row in mutable_mask
 31                    ))
 32                    masks.add(mask)
 33        return masks
 34
 35    def get_fingerings(self, pitch: Pitch) -> set[UnisonoricFingering]:
 36        fingerings = set()
 37
 38        left_all_false = Mask(tuple((False,) * len(row) for row in self.left))
 39        right_all_false = Mask(tuple((False,) * len(row) for row in self.right))
 40
 41        for left_mask in self.__make_masks(pitch, self.left):
 42            fingerings.add(UnisonoricFingering(self, left_mask, right_all_false))
 43        for right_mask in self.__make_masks(pitch, self.right):
 44            fingerings.add(UnisonoricFingering(self, left_all_false, right_mask))
 45        return fingerings
 46
 47    def __str__(self) -> str:
 48        lines = []
 49        for left_row, right_row in zip(self.left, self.right):
 50            cols = []
 51            for button in left_row:
 52                cols.append(str(button).ljust(3))
 53            cols.append('   ')
 54            for button in right_row:
 55                cols.append(str(button).ljust(3))
 56            lines.append(' '.join(cols).strip())
 57        return '\n'.join(lines)
 58
 59    def transpose(self, semitones: int) -> UnisonoricLayout:
 60        return UnisonoricLayout(
 61            self.left.transpose(semitones),
 62            self.right.transpose(semitones))
 63
 64
 65@dataclass(frozen=True)
 66class UnisonoricFingering(Fingering):
 67    layout: UnisonoricLayout
 68    left_mask: Mask
 69    right_mask: Mask
 70
 71    def __str__(self) -> str:
 72        filler = '--- '
 73
 74        def button_down_f(pitch: Pitch) -> str:
 75            return str(pitch).ljust(len(filler))
 76
 77        def button_up_f(pitch: Pitch) -> str:
 78            return filler
 79        return self.format(button_down_f, button_up_f)
 80
 81    def __format_button_row(
 82            self,
 83            layout_row: tuple[Pitch, ...], mask_row: tuple[bool, ...],
 84            button_down_f: PitchToStr, button_up_f: PitchToStr) -> str:
 85        return ''.join(
 86            (button_down_f if button else button_up_f)(pitch)
 87            for pitch, button in zip(layout_row, mask_row)
 88        )
 89
 90    def format(
 91            self,
 92            button_down_f: PitchToStr = lambda pitch: '@',
 93            button_up_f: PitchToStr = lambda pitch: '.',
 94            direction_f: Callable[[Direction], str] =
 95            lambda direction: direction.name) -> str:
 96        lines = []
 97        enumerated_mask_rows = enumerate(zip(self.left_mask, self.right_mask))
 98        for i, (left_mask_row, right_mask_row) in enumerated_mask_rows:
 99            cols = []
100            cols.append(self.__format_button_row(
101                self.layout.left[i], left_mask_row,
102                button_down_f, button_up_f))
103            cols.append('   ')
104            cols.append(self.__format_button_row(
105                self.layout.right[i], right_mask_row,
106                button_down_f, button_up_f))
107            lines.append(''.join(cols).strip())
108        return '\n'.join(lines)
109
110    def __or__(self, other: Any) -> UnisonoricFingering:
111        if type(self) != type(other):
112            raise TypeError('mixed operand types')
113        if self.layout != other.layout:
114            raise ValueError('different layouts')
115        return UnisonoricFingering(
116            self.layout,
117            self.left_mask | other.left_mask,
118            self.right_mask | other.right_mask
119        )
120
121    def get_pitches(self) -> set[Pitch]:
122        pitches = set()
123        sides = [
124            (self.layout.left, self.left_mask),
125            (self.layout.right, self.right_mask),
126        ]
127        for side in sides:
128            for layout_row, mask_row in zip(*side):
129                for pitch, button in zip(layout_row, mask_row):
130                    if button:
131                        pitches.add(pitch)
132        return pitches
@dataclass(frozen=True)
class UnisonoricLayout(concertina_helper.layouts.base_classes.Layout[ForwardRef('UnisonoricFingering')]):
11@dataclass(frozen=True)
12class UnisonoricLayout(Layout['UnisonoricFingering']):
13    left: PitchMatrix
14    right: PitchMatrix
15
16    @property
17    def shape(self) -> Shape:
18        return (
19            [len(row) for row in self.left],
20            [len(row) for row in self.right],
21        )
22
23    def __make_masks(self, pitch: Pitch, pm: PitchMatrix) -> set[Mask]:
24        masks = set()
25        for i, row in enumerate(pm):
26            for j, button in enumerate(row):
27                if pitch == button:
28                    mutable_mask = [[False] * len(row) for row in pm]
29                    mutable_mask[i][j] = True
30                    mask = Mask(tuple(
31                        tuple(row) for row in mutable_mask
32                    ))
33                    masks.add(mask)
34        return masks
35
36    def get_fingerings(self, pitch: Pitch) -> set[UnisonoricFingering]:
37        fingerings = set()
38
39        left_all_false = Mask(tuple((False,) * len(row) for row in self.left))
40        right_all_false = Mask(tuple((False,) * len(row) for row in self.right))
41
42        for left_mask in self.__make_masks(pitch, self.left):
43            fingerings.add(UnisonoricFingering(self, left_mask, right_all_false))
44        for right_mask in self.__make_masks(pitch, self.right):
45            fingerings.add(UnisonoricFingering(self, left_all_false, right_mask))
46        return fingerings
47
48    def __str__(self) -> str:
49        lines = []
50        for left_row, right_row in zip(self.left, self.right):
51            cols = []
52            for button in left_row:
53                cols.append(str(button).ljust(3))
54            cols.append('   ')
55            for button in right_row:
56                cols.append(str(button).ljust(3))
57            lines.append(' '.join(cols).strip())
58        return '\n'.join(lines)
59
60    def transpose(self, semitones: int) -> UnisonoricLayout:
61        return UnisonoricLayout(
62            self.left.transpose(semitones),
63            self.right.transpose(semitones))
shape: tuple[typing.Iterable[int], typing.Iterable[int]]

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

36    def get_fingerings(self, pitch: Pitch) -> set[UnisonoricFingering]:
37        fingerings = set()
38
39        left_all_false = Mask(tuple((False,) * len(row) for row in self.left))
40        right_all_false = Mask(tuple((False,) * len(row) for row in self.right))
41
42        for left_mask in self.__make_masks(pitch, self.left):
43            fingerings.add(UnisonoricFingering(self, left_mask, right_all_false))
44        for right_mask in self.__make_masks(pitch, self.right):
45            fingerings.add(UnisonoricFingering(self, left_all_false, right_mask))
46        return fingerings

Returns a set of possible fingerings on the layout for a given pitch

def transpose( self, semitones: int) -> concertina_helper.layouts.unisonoric.UnisonoricLayout:
60    def transpose(self, semitones: int) -> UnisonoricLayout:
61        return UnisonoricLayout(
62            self.left.transpose(semitones),
63            self.right.transpose(semitones))

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

@dataclass(frozen=True)
class UnisonoricFingering(concertina_helper.layouts.base_classes.Fingering):
 66@dataclass(frozen=True)
 67class UnisonoricFingering(Fingering):
 68    layout: UnisonoricLayout
 69    left_mask: Mask
 70    right_mask: Mask
 71
 72    def __str__(self) -> str:
 73        filler = '--- '
 74
 75        def button_down_f(pitch: Pitch) -> str:
 76            return str(pitch).ljust(len(filler))
 77
 78        def button_up_f(pitch: Pitch) -> str:
 79            return filler
 80        return self.format(button_down_f, button_up_f)
 81
 82    def __format_button_row(
 83            self,
 84            layout_row: tuple[Pitch, ...], mask_row: tuple[bool, ...],
 85            button_down_f: PitchToStr, button_up_f: PitchToStr) -> str:
 86        return ''.join(
 87            (button_down_f if button else button_up_f)(pitch)
 88            for pitch, button in zip(layout_row, mask_row)
 89        )
 90
 91    def format(
 92            self,
 93            button_down_f: PitchToStr = lambda pitch: '@',
 94            button_up_f: PitchToStr = lambda pitch: '.',
 95            direction_f: Callable[[Direction], str] =
 96            lambda direction: direction.name) -> str:
 97        lines = []
 98        enumerated_mask_rows = enumerate(zip(self.left_mask, self.right_mask))
 99        for i, (left_mask_row, right_mask_row) in enumerated_mask_rows:
100            cols = []
101            cols.append(self.__format_button_row(
102                self.layout.left[i], left_mask_row,
103                button_down_f, button_up_f))
104            cols.append('   ')
105            cols.append(self.__format_button_row(
106                self.layout.right[i], right_mask_row,
107                button_down_f, button_up_f))
108            lines.append(''.join(cols).strip())
109        return '\n'.join(lines)
110
111    def __or__(self, other: Any) -> UnisonoricFingering:
112        if type(self) != type(other):
113            raise TypeError('mixed operand types')
114        if self.layout != other.layout:
115            raise ValueError('different layouts')
116        return UnisonoricFingering(
117            self.layout,
118            self.left_mask | other.left_mask,
119            self.right_mask | other.right_mask
120        )
121
122    def get_pitches(self) -> set[Pitch]:
123        pitches = set()
124        sides = [
125            (self.layout.left, self.left_mask),
126            (self.layout.right, self.right_mask),
127        ]
128        for side in sides:
129            for layout_row, mask_row in zip(*side):
130                for pitch, button in zip(layout_row, mask_row):
131                    if button:
132                        pitches.add(pitch)
133        return pitches
def format( self, button_down_f: collections.abc.Callable[[concertina_helper.type_defs.Pitch], str] = <function UnisonoricFingering.<lambda>>, button_up_f: collections.abc.Callable[[concertina_helper.type_defs.Pitch], str] = <function UnisonoricFingering.<lambda>>, direction_f: collections.abc.Callable[[concertina_helper.type_defs.Direction], str] = <function UnisonoricFingering.<lambda>>) -> str:
 91    def format(
 92            self,
 93            button_down_f: PitchToStr = lambda pitch: '@',
 94            button_up_f: PitchToStr = lambda pitch: '.',
 95            direction_f: Callable[[Direction], str] =
 96            lambda direction: direction.name) -> str:
 97        lines = []
 98        enumerated_mask_rows = enumerate(zip(self.left_mask, self.right_mask))
 99        for i, (left_mask_row, right_mask_row) in enumerated_mask_rows:
100            cols = []
101            cols.append(self.__format_button_row(
102                self.layout.left[i], left_mask_row,
103                button_down_f, button_up_f))
104            cols.append('   ')
105            cols.append(self.__format_button_row(
106                self.layout.right[i], right_mask_row,
107                button_down_f, button_up_f))
108            lines.append(''.join(cols).strip())
109        return '\n'.join(lines)

Returns a formatted, human-readable string

def get_pitches(self) -> set[concertina_helper.type_defs.Pitch]:
122    def get_pitches(self) -> set[Pitch]:
123        pitches = set()
124        sides = [
125            (self.layout.left, self.left_mask),
126            (self.layout.right, self.right_mask),
127        ]
128        for side in sides:
129            for layout_row, mask_row in zip(*side):
130                for pitch, button in zip(layout_row, mask_row):
131                    if button:
132                        pitches.add(pitch)
133        return pitches

Returns the pitches that would be produced by this fingering