kernpy 0.0.1__py3-none-any.whl → 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. kernpy/__init__.py +215 -0
  2. kernpy/__main__.py +217 -0
  3. kernpy/core/__init__.py +119 -0
  4. kernpy/core/_io.py +48 -0
  5. kernpy/core/base_antlr_importer.py +61 -0
  6. kernpy/core/base_antlr_spine_parser_listener.py +196 -0
  7. kernpy/core/basic_spine_importer.py +43 -0
  8. kernpy/core/document.py +965 -0
  9. kernpy/core/dyn_importer.py +30 -0
  10. kernpy/core/dynam_spine_importer.py +42 -0
  11. kernpy/core/error_listener.py +51 -0
  12. kernpy/core/exporter.py +535 -0
  13. kernpy/core/fing_spine_importer.py +42 -0
  14. kernpy/core/generated/kernSpineLexer.interp +444 -0
  15. kernpy/core/generated/kernSpineLexer.py +535 -0
  16. kernpy/core/generated/kernSpineLexer.tokens +236 -0
  17. kernpy/core/generated/kernSpineParser.interp +425 -0
  18. kernpy/core/generated/kernSpineParser.py +9954 -0
  19. kernpy/core/generated/kernSpineParser.tokens +236 -0
  20. kernpy/core/generated/kernSpineParserListener.py +1200 -0
  21. kernpy/core/generated/kernSpineParserVisitor.py +673 -0
  22. kernpy/core/generic.py +426 -0
  23. kernpy/core/gkern.py +526 -0
  24. kernpy/core/graphviz_exporter.py +89 -0
  25. kernpy/core/harm_spine_importer.py +41 -0
  26. kernpy/core/import_humdrum_old.py +853 -0
  27. kernpy/core/importer.py +285 -0
  28. kernpy/core/importer_factory.py +43 -0
  29. kernpy/core/kern_spine_importer.py +73 -0
  30. kernpy/core/mens_spine_importer.py +23 -0
  31. kernpy/core/mhxm_spine_importer.py +44 -0
  32. kernpy/core/pitch_models.py +338 -0
  33. kernpy/core/root_spine_importer.py +58 -0
  34. kernpy/core/spine_importer.py +45 -0
  35. kernpy/core/text_spine_importer.py +43 -0
  36. kernpy/core/tokenizers.py +239 -0
  37. kernpy/core/tokens.py +2011 -0
  38. kernpy/core/transposer.py +300 -0
  39. kernpy/io/__init__.py +14 -0
  40. kernpy/io/public.py +355 -0
  41. kernpy/polish_scores/__init__.py +13 -0
  42. kernpy/polish_scores/download_polish_dataset.py +357 -0
  43. kernpy/polish_scores/iiif.py +47 -0
  44. kernpy/test_grammar.sh +22 -0
  45. kernpy/util/__init__.py +14 -0
  46. kernpy/util/helpers.py +55 -0
  47. kernpy/util/store_cache.py +35 -0
  48. kernpy/visualize_analysis.sh +23 -0
  49. kernpy-1.0.0.dist-info/METADATA +501 -0
  50. kernpy-1.0.0.dist-info/RECORD +51 -0
  51. {kernpy-0.0.1.dist-info → kernpy-1.0.0.dist-info}/WHEEL +1 -2
  52. kernpy/example.py +0 -0
  53. kernpy-0.0.1.dist-info/LICENSE +0 -19
  54. kernpy-0.0.1.dist-info/METADATA +0 -19
  55. kernpy-0.0.1.dist-info/RECORD +0 -7
  56. kernpy-0.0.1.dist-info/top_level.txt +0 -1
@@ -0,0 +1,338 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from enum import Enum
5
+
6
+
7
+ pitches = {
8
+ 'A',
9
+ 'B',
10
+ 'C',
11
+ 'D',
12
+ 'E',
13
+ 'F',
14
+ 'G'
15
+ }
16
+
17
+
18
+ Chromas = {
19
+ 'C--': 0,
20
+ 'C-': 1,
21
+ 'C': 2,
22
+ 'C+': 3,
23
+ 'C++': 4,
24
+ 'D---': 5,
25
+ 'D--': 6,
26
+ 'D-': 7,
27
+ 'D': 8,
28
+ 'D+': 9,
29
+ 'D++': 10,
30
+ 'E---': 11,
31
+ 'E--': 12,
32
+ 'E-': 13,
33
+ 'E': 14,
34
+ 'E+': 15,
35
+ 'E++': 16,
36
+ 'F--': 17,
37
+ 'F-': 18,
38
+ 'F': 19,
39
+ 'F+': 20,
40
+ 'F++': 21,
41
+ # 22 is unused
42
+ 'G--': 23,
43
+ 'G-': 24,
44
+ 'G': 25,
45
+ 'G+': 26,
46
+ 'G++': 27,
47
+ 'A---': 28,
48
+ 'A--': 29,
49
+ 'A-': 30,
50
+ 'A': 31,
51
+ 'A+': 32,
52
+ 'A++': 33,
53
+ 'B---': 34,
54
+ 'B--': 35,
55
+ 'B-': 36,
56
+ 'B': 37,
57
+ 'B+': 38,
58
+ 'B++': 39
59
+ }
60
+
61
+ ChromasByValue = {v: k for k, v in Chromas.items()} # reverse the key-value pairs
62
+
63
+ class NotationEncoding(Enum):
64
+ AMERICAN = 'american'
65
+ HUMDRUM = 'kern'
66
+
67
+ class Direction(Enum):
68
+ UP = 'up'
69
+ DOWN = 'down'
70
+
71
+
72
+ class AgnosticPitch:
73
+ """
74
+ Represents a pitch in a generic way, independent of the notation system used.
75
+ """
76
+
77
+ ASCENDANT_ACCIDENTAL_ALTERATION = '+'
78
+ DESCENDENT_ACCIDENTAL_ALTERATION = '-'
79
+ ACCIDENTAL_ALTERATIONS = {
80
+ ASCENDANT_ACCIDENTAL_ALTERATION,
81
+ DESCENDENT_ACCIDENTAL_ALTERATION
82
+ }
83
+
84
+
85
+ def __init__(self, name: str, octave: int):
86
+ """
87
+ Initialize the AgnosticPitch object.
88
+
89
+ Args:
90
+ name (str): The name of the pitch (e.g., 'C', 'D#', 'Bb').
91
+ octave (int): The octave of the pitch (e.g., 4 for middle C).
92
+ """
93
+ self.name = name
94
+ self.octave = octave
95
+
96
+ @property
97
+ def name(self):
98
+ return self.__name
99
+
100
+ @name.setter
101
+ def name(self, name):
102
+ accidentals = ''.join([c for c in name if c in ['-', '+']])
103
+ name = name.upper()
104
+ name = name.replace('#', '+').replace('b', '-')
105
+
106
+ check_name = name.replace('+', '').replace('-', '')
107
+ if check_name not in pitches:
108
+ raise ValueError(f"Invalid pitch: {name}")
109
+ if len(accidentals) > 3:
110
+ raise ValueError(f"Invalid pitch: {name}. Maximum of 3 accidentals allowed. ")
111
+ self.__name = name
112
+
113
+ @property
114
+ def octave(self):
115
+ return self.__octave
116
+
117
+ @octave.setter
118
+ def octave(self, octave):
119
+ if not isinstance(octave, int):
120
+ raise ValueError(f"Invalid octave: {octave}")
121
+ self.__octave = octave
122
+
123
+ def get_chroma(self):
124
+ return 40 * self.octave + Chromas[self.name]
125
+
126
+ @classmethod
127
+ def to_transposed(cls, agnostic_pitch: 'AgnosticPitch', raw_interval, direction: str = Direction.UP.value) -> 'AgnosticPitch':
128
+ delta = raw_interval if direction == Direction.UP.value else - raw_interval
129
+ chroma = agnostic_pitch.get_chroma() + delta
130
+ name = ChromasByValue[chroma % 40]
131
+ octave = chroma // 40
132
+ return AgnosticPitch(name, octave)
133
+
134
+ @classmethod
135
+ def get_chroma_from_interval(cls, pitch_a: 'AgnosticPitch', pitch_b: 'AgnosticPitch'):
136
+ return pitch_b.get_chroma() - pitch_a.get_chroma()
137
+
138
+ def __str__(self):
139
+ return f"<{self.name}, {self.octave}>"
140
+
141
+ def __repr__(self):
142
+ return f"{self.__name}(name={self.name}, octave={self.octave})"
143
+
144
+ def __eq__(self, other):
145
+ if not isinstance(other, AgnosticPitch):
146
+ return False
147
+ return self.name == other.name and self.octave == other.octave
148
+
149
+ def __ne__(self, other):
150
+ if not isinstance(other, AgnosticPitch):
151
+ return True
152
+ return self.name != other.name or self.octave != other.octave
153
+
154
+ def __hash__(self):
155
+ return hash((self.name, self.octave))
156
+
157
+ def __lt__(self, other):
158
+ if not isinstance(other, AgnosticPitch):
159
+ return NotImplemented
160
+ if self.octave == other.octave:
161
+ return Chromas[self.name] < Chromas[other.name]
162
+ return self.octave < other.octave
163
+
164
+ def __gt__(self, other):
165
+ if not isinstance(other, AgnosticPitch):
166
+ return NotImplemented
167
+ if self.octave == other.octave:
168
+ return Chromas[self.name] > Chromas[other.name]
169
+ return self.octave > other.octave
170
+
171
+
172
+
173
+ class PitchImporter(ABC):
174
+ def __init__(self):
175
+ self.octave = None
176
+ self.name = None
177
+
178
+ @abstractmethod
179
+ def import_pitch(self, encoding: str) -> AgnosticPitch:
180
+ pass
181
+
182
+ @abstractmethod
183
+ def _parse_pitch(self, pitch: str):
184
+ pass
185
+
186
+ class HumdrumPitchImporter(PitchImporter):
187
+ """
188
+ Represents the pitch in the Humdrum Kern format.
189
+
190
+ The name is represented using the International Standard Organization (ISO) name notation.
191
+ The first line below the staff is the C4 in G clef. The above C is C5, the below C is C3, etc.
192
+
193
+ The Humdrum Kern format uses the following name representation:
194
+ 'c' = C4
195
+ 'cc' = C5
196
+ 'ccc' = C6
197
+ 'cccc' = C7
198
+
199
+ 'C' = C3
200
+ 'CC' = C2
201
+ 'CCC' = C1
202
+
203
+ This class do not limit the name ranges.
204
+
205
+ In the following example, the name is represented by the letter 'c'. The name of 'c' is C4, 'cc' is C5, 'ccc' is C6.
206
+ ```
207
+ **kern
208
+ *clefG2
209
+ 2c // C4
210
+ 2cc // C5
211
+ 2ccc // C6
212
+ 2C // C3
213
+ 2CC // C2
214
+ 2CCC // C1
215
+ *-
216
+ ```
217
+ """
218
+ C4_PITCH_LOWERCASE = 'c'
219
+ C4_OCATAVE = 4
220
+ C3_PITCH_UPPERCASE = 'C'
221
+ C3_OCATAVE = 3
222
+ VALID_PITCHES = 'abcdefg' + 'ABCDEFG'
223
+
224
+ def __init__(self):
225
+ super().__init__()
226
+
227
+ def import_pitch(self, encoding: str) -> AgnosticPitch:
228
+ self.name, self.octave = self._parse_pitch(encoding)
229
+ return AgnosticPitch(self.name, self.octave)
230
+
231
+ def _parse_pitch(self, encoding: str) -> tuple:
232
+ accidentals = ''.join([c for c in encoding if c in ['#', '-']])
233
+ accidentals = accidentals.replace('#', '+')
234
+ encoding = encoding.replace('#', '').replace('-', '')
235
+ pitch = encoding[0].lower()
236
+ octave = None
237
+ if encoding[0].islower():
238
+ min_octave = HumdrumPitchImporter.C4_OCATAVE
239
+ octave = min_octave + (len(encoding) - 1)
240
+ elif encoding[0].isupper():
241
+ max_octave = HumdrumPitchImporter.C3_OCATAVE
242
+ octave = max_octave - (len(encoding) - 1)
243
+ name = f"{pitch}{accidentals}"
244
+ return name, octave
245
+
246
+
247
+ class AmericanPitchImporter(PitchImporter):
248
+ def __init__(self):
249
+ super().__init__()
250
+
251
+ def import_pitch(self, encoding: str) -> AgnosticPitch:
252
+ self.name, self.octave = self._parse_pitch(encoding)
253
+ return AgnosticPitch(self.name, self.octave)
254
+
255
+ def _parse_pitch(self, encoding: str):
256
+ octave = int(''.join([n for n in encoding if n.isnumeric()]))
257
+ chroma = ''.join([c.lower() for c in encoding if c.isalpha() or c in ['-', '+', '#', 'b']])
258
+
259
+ return chroma, octave
260
+
261
+
262
+ class PitchImporterFactory:
263
+ @classmethod
264
+ def create(cls, encoding: str) -> PitchImporter:
265
+ if encoding == NotationEncoding.AMERICAN.value:
266
+ return AmericanPitchImporter()
267
+ elif encoding == NotationEncoding.HUMDRUM.value:
268
+ return HumdrumPitchImporter()
269
+ else:
270
+ raise ValueError(f"Invalid encoding: {encoding}. \nUse one of {NotationEncoding.__members__.values()}")
271
+
272
+
273
+ class PitchExporter(ABC):
274
+ def __init__(self):
275
+ self.pitch = None
276
+
277
+ @abstractmethod
278
+ def export_pitch(self, pitch: AgnosticPitch) -> str:
279
+ pass
280
+
281
+ def _is_valid_pitch(self):
282
+ clean_pitch = ''.join([c for c in self.pitch.name if c.isalpha()])
283
+ clean_pitch = clean_pitch.upper()
284
+ if len(clean_pitch) > 1:
285
+ clean_pitch = clean_pitch[0]
286
+ return clean_pitch in pitches
287
+
288
+
289
+ class HumdrumPitchExporter(PitchExporter):
290
+ C4_PITCH_LOWERCASE = 'c'
291
+ C4_OCATAVE = 4
292
+ C3_PITCH_UPPERCASE = 'C'
293
+ C3_OCATAVE = 3
294
+
295
+ def __init__(self):
296
+ super().__init__()
297
+
298
+ def export_pitch(self, pitch: AgnosticPitch) -> str:
299
+ accidentals = ''.join([c for c in pitch.name if c in ['-', '+']])
300
+ accidentals = accidentals.replace('+', '#')
301
+ accidentals_output = len(accidentals) * accidentals[0] if len(accidentals) > 0 else ''
302
+ pitch.name = pitch.name.replace('+', '').replace('-', '')
303
+
304
+ if pitch.octave >= HumdrumPitchExporter.C4_OCATAVE:
305
+ return f"{pitch.name.lower() * (pitch.octave - HumdrumPitchExporter.C4_OCATAVE + 1)}{accidentals_output}"
306
+ else:
307
+ return f"{pitch.name.upper() * (HumdrumPitchExporter.C3_OCATAVE - pitch.octave + 1)}{accidentals_output}"
308
+
309
+
310
+ class AmericanPitchExporter(PitchExporter):
311
+ def __init__(self):
312
+ super().__init__()
313
+
314
+ def export_pitch(self, pitch: AgnosticPitch) -> str:
315
+ self.pitch = pitch
316
+
317
+ if not self._is_valid_pitch():
318
+ raise ValueError(f"Invalid pitch: {self.pitch.name}")
319
+
320
+ clean_name = ''.join([c for c in self.pitch.name if c.isalpha()])
321
+ clean_name = clean_name.upper()
322
+ accidentals = ''.join([c for c in self.pitch.name if c in ['-', '+']])
323
+ total_accidentals = len(accidentals)
324
+ accidentals_output = ''
325
+ if total_accidentals > 0:
326
+ accidentals_output = total_accidentals * '#' if accidentals == '+' else total_accidentals * 'b'
327
+ return f"{clean_name}{accidentals_output}{self.pitch.octave}"
328
+
329
+
330
+ class PitchExporterFactory:
331
+ @classmethod
332
+ def create(cls, encoding: str) -> PitchExporter:
333
+ if encoding == NotationEncoding.AMERICAN.value:
334
+ return AmericanPitchExporter()
335
+ elif encoding == NotationEncoding.HUMDRUM.value:
336
+ return HumdrumPitchExporter()
337
+ else:
338
+ raise ValueError(f"Invalid encoding: {encoding}. \nUse one of {NotationEncoding.__members__.values()}")
@@ -0,0 +1,58 @@
1
+ from __future__ import annotations
2
+ from typing import Optional
3
+
4
+ from antlr4 import InputStream, CommonTokenStream, ParseTreeWalker, BailErrorStrategy, \
5
+ PredictionMode
6
+
7
+ from .base_antlr_importer import BaseANTLRListenerImporter
8
+ from .base_antlr_spine_parser_listener import BaseANTLRSpineParserListener
9
+ from .error_listener import ErrorListener
10
+ from .kern_spine_importer import KernSpineListener, KernSpineImporter
11
+ from .spine_importer import SpineImporter
12
+ from .generated.kernSpineLexer import kernSpineLexer
13
+ from .generated.kernSpineParser import kernSpineParser
14
+ from .tokens import TokenCategory, Token, SimpleToken
15
+
16
+
17
+ class RootSpineListener(BaseANTLRSpineParserListener):
18
+ def __init__(self):
19
+ super().__init__()
20
+
21
+
22
+ class RootListenerImporter(BaseANTLRListenerImporter):
23
+
24
+ def createListener(self):
25
+ return KernSpineListener()
26
+
27
+ def createLexer(self, tokenStream):
28
+ return kernSpineLexer(tokenStream)
29
+
30
+ def createParser(self, tokenStream):
31
+ return kernSpineParser(tokenStream)
32
+
33
+ def startRule(self):
34
+ return self.parser.start()
35
+
36
+
37
+ class RootSpineImporter(SpineImporter):
38
+ def __init__(self, verbose: Optional[bool] = False):
39
+ """
40
+ KernSpineImporter constructor.
41
+
42
+ Args:
43
+ verbose (Optional[bool]): Level of verbosity for error messages.
44
+ """
45
+ super().__init__(verbose=verbose)
46
+
47
+ def import_listener(self) -> BaseANTLRSpineParserListener:
48
+ #return RootSpineListener() # TODO: Create a custom functional listener for RootSpineImporter
49
+ return KernSpineListener()
50
+
51
+ def import_token(self, encoding: str) -> Token:
52
+ self._raise_error_if_wrong_input(encoding)
53
+
54
+ kern_spine_importer = KernSpineImporter()
55
+ token = kern_spine_importer.import_token(encoding)
56
+
57
+ return token # The **root spine tokens are always a subset of the **kern spine tokens
58
+
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Optional
5
+
6
+ from antlr4 import InputStream, CommonTokenStream, ParseTreeWalker, BailErrorStrategy, \
7
+ PredictionMode
8
+
9
+ from .generated.kernSpineLexer import kernSpineLexer
10
+ from .generated.kernSpineParser import kernSpineParser
11
+ from .base_antlr_spine_parser_listener import BaseANTLRSpineParserListener
12
+ from .error_listener import ErrorListener
13
+ from .tokens import Token
14
+
15
+
16
+ class SpineImporter(ABC):
17
+ def __init__(self, verbose: Optional[bool] = False):
18
+ """
19
+ SpineImporter constructor.
20
+ This class is an abstract base class for importing all kinds of spines.
21
+
22
+ Args:
23
+ verbose (Optional[bool]): Level of verbosity for error messages.
24
+ """
25
+ self.import_listener = self.import_listener()
26
+ self.error_listener = ErrorListener(verbose=verbose)
27
+
28
+ @abstractmethod
29
+ def import_listener(self) -> BaseANTLRSpineParserListener:
30
+ pass
31
+
32
+ @abstractmethod
33
+ def import_token(self, encoding: str) -> Token:
34
+ pass
35
+
36
+ @classmethod
37
+ def _raise_error_if_wrong_input(cls, encoding: str):
38
+ if encoding is None:
39
+ raise ValueError("Encoding cannot be None")
40
+ if not isinstance(encoding, str):
41
+ raise TypeError("Encoding must be a string")
42
+ if encoding == '':
43
+ raise ValueError("Encoding cannot be an empty string")
44
+
45
+
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+ from typing import Optional
3
+
4
+ from .base_antlr_spine_parser_listener import BaseANTLRSpineParserListener
5
+ from .spine_importer import SpineImporter
6
+ from .spine_importer import SpineImporter
7
+ from .kern_spine_importer import KernSpineImporter, KernSpineListener
8
+ from .tokens import SimpleToken, TokenCategory, Token
9
+
10
+
11
+ class TextSpineImporter(SpineImporter):
12
+ def __init__(self, verbose: Optional[bool] = False):
13
+ """
14
+ KernSpineImporter constructor.
15
+
16
+ Args:
17
+ verbose (Optional[bool]): Level of verbosity for error messages.
18
+ """
19
+ super().__init__(verbose=verbose)
20
+
21
+ def import_listener(self) -> BaseANTLRSpineParserListener:
22
+ return KernSpineListener() # TODO: Create a custom functional listener for TextSpineImporter
23
+
24
+ def import_token(self, encoding: str) -> Token:
25
+ self._raise_error_if_wrong_input(encoding)
26
+
27
+ kern_spine_importer = KernSpineImporter()
28
+ token = kern_spine_importer.import_token(encoding)
29
+
30
+ ACCEPTED_CATEGORIES = {
31
+ TokenCategory.STRUCTURAL,
32
+ TokenCategory.SIGNATURES,
33
+ TokenCategory.EMPTY,
34
+ TokenCategory.BARLINES,
35
+ TokenCategory.IMAGE_ANNOTATIONS,
36
+ TokenCategory.BARLINES,
37
+ TokenCategory.COMMENTS,
38
+ }
39
+
40
+ if not any(TokenCategory.is_child(child=token.category, parent=cat) for cat in ACCEPTED_CATEGORIES):
41
+ return SimpleToken(encoding, TokenCategory.LYRICS)
42
+
43
+ return token