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
kernpy/core/tokens.py ADDED
@@ -0,0 +1,2011 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from collections.abc import Sequence
5
+ from enum import Enum, auto
6
+ import copy
7
+ from typing import List, Dict, Set, Union, Optional
8
+ from unittest import result
9
+
10
+ TOKEN_SEPARATOR = '@'
11
+ DECORATION_SEPARATOR = '·'
12
+ GRAPHIC_TOKEN_SEPARATOR = ':'
13
+ HEADERS = {"**mens", "**kern", "**text", "**harm", "**mxhm", "**root", "**dyn", "**dynam", "**fing"}
14
+ CORE_HEADERS = {"**kern", "**mens"}
15
+ SPINE_OPERATIONS = {"*-", "*+", "*^", "*v", "*x"}
16
+ TERMINATOR = "*-"
17
+ EMPTY_TOKEN = "*"
18
+ ERROR_TOKEN = "Z"
19
+
20
+
21
+ # We don't use inheritance here for all elements but enum, because we don't need any polymorphism mechanism, just a grouping one
22
+ # TODO Poner todos los tipos - p.ej. también comandos de layout - slurs, etc...
23
+ class TokenCategory(Enum):
24
+ """
25
+ Options for the category of a token.
26
+
27
+ This is used to determine what kind of token should be exported.
28
+
29
+ The categories are sorted the specific order they are compared to sorthem. But hierarchical order must be defined in other data structures.
30
+ """
31
+ STRUCTURAL = auto() # header, spine operations
32
+ HEADER = auto() # **kern, **mens, **text, **harm, **mxhm, **root, **dyn, **dynam, **fing
33
+ SPINE_OPERATION = auto()
34
+ CORE = auto() # notes, rests, chords, etc.
35
+ ERROR = auto()
36
+ NOTE_REST = auto()
37
+ NOTE = auto()
38
+ DURATION = auto()
39
+ PITCH = auto()
40
+ ALTERATION = auto()
41
+ DECORATION = auto()
42
+ REST = auto()
43
+ CHORD = auto()
44
+ EMPTY = auto() # placeholders, null interpretation
45
+ SIGNATURES = auto()
46
+ CLEF = auto()
47
+ TIME_SIGNATURE = auto()
48
+ METER_SYMBOL = auto()
49
+ KEY_SIGNATURE = auto()
50
+ KEY_TOKEN = auto()
51
+ ENGRAVED_SYMBOLS = auto()
52
+ OTHER_CONTEXTUAL = auto()
53
+ BARLINES = auto()
54
+ COMMENTS = auto()
55
+ FIELD_COMMENTS = auto()
56
+ LINE_COMMENTS = auto()
57
+ DYNAMICS = auto()
58
+ HARMONY = auto()
59
+ FINGERING = auto()
60
+ LYRICS = auto()
61
+ INSTRUMENTS = auto()
62
+ IMAGE_ANNOTATIONS = auto()
63
+ BOUNDING_BOXES = auto()
64
+ LINE_BREAK = auto()
65
+ OTHER = auto()
66
+ MHXM = auto()
67
+ ROOT = auto()
68
+
69
+ def __lt__(self, other):
70
+ """
71
+ Compare two TokenCategory.
72
+ Args:
73
+ other (TokenCategory): The other category to compare.
74
+
75
+ Returns (bool): True if this category is lower than the other, False otherwise.
76
+
77
+ Examples:
78
+ >>> TokenCategory.STRUCTURAL < TokenCategory.CORE
79
+ True
80
+ >>> TokenCategory.STRUCTURAL < TokenCategory.STRUCTURAL
81
+ False
82
+ >>> TokenCategory.CORE < TokenCategory.STRUCTURAL
83
+ False
84
+ >>> sorted([TokenCategory.STRUCTURAL, TokenCategory.CORE])
85
+ [TokenCategory.STRUCTURAL, TokenCategory.CORE]
86
+ """
87
+ if isinstance(other, TokenCategory):
88
+ return self.value < other.value
89
+ return NotImplemented
90
+
91
+ @classmethod
92
+ def all(cls) -> Set[TokenCategory]:
93
+ f"""
94
+ Get all categories in the hierarchy.
95
+
96
+ Returns:
97
+ Set[TokenCategory]: The set of all categories in the hierarchy.
98
+
99
+ Examples:
100
+ >>> import kernpy as kp
101
+ >>> kp.TokenCategory.all()
102
+ set([<TokenCategory.MHXM: 29>, <TokenCategory.COMMENTS: 19>, <TokenCategory.BARLINES: 18>, <TokenCategory.CORE: 2>, <TokenCategory.BOUNDING_BOXES: 27>, <TokenCategory.NOTE_REST: 3>, <TokenCategory.NOTE: 4>, <TokenCategory.ENGRAVED_SYMBOLS: 16>, <TokenCategory.SIGNATURES: 11>, <TokenCategory.REST: 8>, <TokenCategory.METER_SYMBOL: 14>, <TokenCategory.HARMONY: 23>, <TokenCategory.KEY_SIGNATURE: 15>, <TokenCategory.EMPTY: 10>, <TokenCategory.PITCH: 6>, <TokenCategory.LINE_COMMENTS: 21>, <TokenCategory.FINGERING: 24>, <TokenCategory.DECORATION: 7>, <TokenCategory.OTHER: 28>, <TokenCategory.INSTRUMENTS: 26>, <TokenCategory.STRUCTURAL: 1>, <TokenCategory.FIELD_COMMENTS: 20>, <TokenCategory.LYRICS: 25>, <TokenCategory.CLEF: 12>, <TokenCategory.DURATION: 5>, <TokenCategory.DYNAMICS: 22>, <TokenCategory.CHORD: 9>, <TokenCategory.TIME_SIGNATURE: 13>, <TokenCategory.OTHER_CONTEXTUAL: 17>])
103
+ """
104
+ return set([t for t in TokenCategory])
105
+
106
+ @classmethod
107
+ def tree(cls):
108
+ """
109
+ Return a string representation of the category hierarchy
110
+ Returns (str): The string representation of the category hierarchy
111
+
112
+ Examples:
113
+ >>> import kernpy as kp
114
+ >>> print(kp.TokenCategory.tree())
115
+ .
116
+ ├── TokenCategory.STRUCTURAL
117
+ ├── TokenCategory.CORE
118
+ │ ├── TokenCategory.NOTE_REST
119
+ │ │ ├── TokenCategory.DURATION
120
+ │ │ ├── TokenCategory.NOTE
121
+ │ │ │ ├── TokenCategory.PITCH
122
+ │ │ │ └── TokenCategory.DECORATION
123
+ │ │ └── TokenCategory.REST
124
+ │ ├── TokenCategory.CHORD
125
+ │ └── TokenCategory.EMPTY
126
+ ├── TokenCategory.SIGNATURES
127
+ │ ├── TokenCategory.CLEF
128
+ │ ├── TokenCategory.TIME_SIGNATURE
129
+ │ ├── TokenCategory.METER_SYMBOL
130
+ │ └── TokenCategory.KEY_SIGNATURE
131
+ ├── TokenCategory.ENGRAVED_SYMBOLS
132
+ ├── TokenCategory.OTHER_CONTEXTUAL
133
+ ├── TokenCategory.BARLINES
134
+ ├── TokenCategory.COMMENTS
135
+ │ ├── TokenCategory.FIELD_COMMENTS
136
+ │ └── TokenCategory.LINE_COMMENTS
137
+ ├── TokenCategory.DYNAMICS
138
+ ├── TokenCategory.HARMONY
139
+ ├── TokenCategory.FINGERING
140
+ ├── TokenCategory.LYRICS
141
+ ├── TokenCategory.INSTRUMENTS
142
+ ├── TokenCategory.BOUNDING_BOXES
143
+ └── TokenCategory.OTHER
144
+ """
145
+ return TokenCategoryHierarchyMapper.tree()
146
+
147
+ @classmethod
148
+ def is_child(cls, *, child: TokenCategory, parent: TokenCategory) -> bool:
149
+ """
150
+ Check if the child category is a child of the parent category.
151
+
152
+ Args:
153
+ child (TokenCategory): The child category.
154
+ parent (TokenCategory): The parent category.
155
+
156
+ Returns (bool): True if the child category is a child of the parent category, False otherwise.
157
+ """
158
+ return TokenCategoryHierarchyMapper.is_child(parent=parent, child=child)
159
+
160
+ @classmethod
161
+ def children(cls, target: TokenCategory) -> Set[TokenCategory]:
162
+ """
163
+ Get the children of the target category.
164
+
165
+ Args:
166
+ target (TokenCategory): The target category.
167
+
168
+ Returns (List[TokenCategory]): The list of child categories of the target category.
169
+ """
170
+ return TokenCategoryHierarchyMapper.children(parent=target)
171
+
172
+ @classmethod
173
+ def valid(cls, *, include: Optional[Set[TokenCategory]] = None, exclude: Optional[Set[TokenCategory]] = None) -> Set[TokenCategory]:
174
+ """
175
+ Get the valid categories based on the include and exclude sets.
176
+
177
+ Args:
178
+ include (Optional[Set[TokenCategory]]): The set of categories to include. Defaults to None. \
179
+ If None, all categories are included.
180
+ exclude (Optional[Set[TokenCategory]]): The set of categories to exclude. Defaults to None. \
181
+ If None, no categories are excluded.
182
+
183
+ Returns (Set[TokenCategory]): The list of valid categories based on the include and exclude sets.
184
+ """
185
+ return TokenCategoryHierarchyMapper.valid(include=include, exclude=exclude)
186
+
187
+ @classmethod
188
+ def leaves(cls, target: TokenCategory) -> Set[TokenCategory]:
189
+ """
190
+ Get the leaves of the subtree of the target category.
191
+
192
+ Args:
193
+ target (TokenCategory): The target category.
194
+
195
+ Returns (List[TokenCategory]): The list of leaf categories of the target category.
196
+ """
197
+ return TokenCategoryHierarchyMapper.leaves(target=target)
198
+
199
+ @classmethod
200
+ def nodes(cls, target: TokenCategory) -> Set[TokenCategory]:
201
+ """
202
+ Get the nodes of the subtree of the target category.
203
+
204
+ Args:
205
+ target (TokenCategory): The target category.
206
+
207
+ Returns (List[TokenCategory]): The list of node categories of the target category.
208
+ """
209
+ return TokenCategoryHierarchyMapper.nodes(parent=target)
210
+
211
+ @classmethod
212
+ def match(cls,
213
+ target: TokenCategory, *,
214
+ include: Optional[Set[TokenCategory]] = None,
215
+ exclude: Optional[Set[TokenCategory]] = None) -> bool:
216
+ """
217
+ Check if the target category matches the include and exclude sets.
218
+
219
+ Args:
220
+ target (TokenCategory): The target category.
221
+ include (Optional[Set[TokenCategory]]): The set of categories to include. Defaults to None. \
222
+ If None, all categories are included.
223
+ exclude (Optional[Set[TokenCategory]]): The set of categories to exclude. Defaults to None. \
224
+ If None, no categories are excluded.
225
+
226
+ Returns (bool): True if the target category matches the include and exclude sets, False otherwise.
227
+ """
228
+ return TokenCategoryHierarchyMapper.match(category=target, include=include, exclude=exclude)
229
+
230
+ def __str__(self):
231
+ """
232
+ Get the string representation of the category.
233
+
234
+ Returns (str): The string representation of the category.
235
+ """
236
+ return self.name
237
+
238
+
239
+ NON_CORE_CATEGORIES = {
240
+ TokenCategory.STRUCTURAL,
241
+ TokenCategory.SIGNATURES,
242
+ TokenCategory.EMPTY,
243
+ TokenCategory.IMAGE_ANNOTATIONS,
244
+ TokenCategory.BARLINES,
245
+ TokenCategory.COMMENTS,
246
+ }
247
+
248
+ BEKERN_CATEGORIES = {
249
+ TokenCategory.STRUCTURAL,
250
+ TokenCategory.CORE,
251
+ TokenCategory.SIGNATURES,
252
+ TokenCategory.BARLINES,
253
+ TokenCategory.IMAGE_ANNOTATIONS,
254
+ }
255
+
256
+
257
+ class TokenCategoryHierarchyMapper:
258
+ """
259
+ Mapping of the TokenCategory hierarchy.
260
+
261
+ This class is used to define the hierarchy of the TokenCategory. Useful related methods are provided.
262
+ """
263
+ """
264
+ The hierarchy of the TokenCategory is a recursive dictionary that defines the parent-child relationships \
265
+ between the categories. It's a tree.
266
+ """
267
+ _hierarchy_typing = Dict[TokenCategory, '_hierarchy_typing']
268
+ hierarchy: _hierarchy_typing = {
269
+ TokenCategory.STRUCTURAL: {
270
+ TokenCategory.HEADER: {}, # each leave must be an empty dictionary
271
+ TokenCategory.SPINE_OPERATION: {},
272
+ },
273
+ TokenCategory.CORE: {
274
+ TokenCategory.NOTE_REST: {
275
+ TokenCategory.DURATION: {},
276
+ TokenCategory.NOTE: {
277
+ TokenCategory.PITCH: {},
278
+ TokenCategory.DECORATION: {},
279
+ TokenCategory.ALTERATION: {},
280
+ },
281
+ TokenCategory.REST: {},
282
+ },
283
+ TokenCategory.CHORD: {},
284
+ TokenCategory.EMPTY: {},
285
+ TokenCategory.ERROR: {},
286
+ },
287
+ TokenCategory.SIGNATURES: {
288
+ TokenCategory.CLEF: {},
289
+ TokenCategory.TIME_SIGNATURE: {},
290
+ TokenCategory.METER_SYMBOL: {},
291
+ TokenCategory.KEY_SIGNATURE: {},
292
+ TokenCategory.KEY_TOKEN: {},
293
+ },
294
+ TokenCategory.ENGRAVED_SYMBOLS: {},
295
+ TokenCategory.OTHER_CONTEXTUAL: {},
296
+ TokenCategory.BARLINES: {},
297
+ TokenCategory.COMMENTS: {
298
+ TokenCategory.FIELD_COMMENTS: {},
299
+ TokenCategory.LINE_COMMENTS: {},
300
+ },
301
+ TokenCategory.DYNAMICS: {},
302
+ TokenCategory.HARMONY: {},
303
+ TokenCategory.FINGERING: {},
304
+ TokenCategory.LYRICS: {},
305
+ TokenCategory.INSTRUMENTS: {},
306
+ TokenCategory.IMAGE_ANNOTATIONS: {
307
+ TokenCategory.BOUNDING_BOXES: {},
308
+ TokenCategory.LINE_BREAK: {},
309
+ },
310
+ TokenCategory.OTHER: {},
311
+ TokenCategory.MHXM: {},
312
+ TokenCategory.ROOT: {},
313
+ }
314
+
315
+ @classmethod
316
+ def _is_child(cls, parent: TokenCategory, child: TokenCategory, *, tree: '_hierarchy_typing') -> bool:
317
+ """
318
+ Recursively check if `child` is in the subtree of `parent`.
319
+
320
+ Args:
321
+ parent (TokenCategory): The parent category.
322
+ child (TokenCategory): The category to check.
323
+ tree (_hierarchy_typing): The subtree to check.
324
+
325
+ Returns:
326
+ bool: True if `child` is a descendant of `parent`, False otherwise.
327
+ """
328
+ # Base case: the parent is empty.
329
+ if len(tree.keys()) == 0:
330
+ return False
331
+
332
+ # Recursive case: explore the direct children of the parent.
333
+ return any(
334
+ direct_child == child or cls._is_child(direct_child, child, tree=tree[parent])
335
+ for direct_child in tree.get(parent, {})
336
+ )
337
+ # Vectorized version of the following code:
338
+ #direct_children = tree.get(parent, dict())
339
+ #for direct_child in direct_children.keys():
340
+ # if direct_child == child or cls._is_child(direct_child, child, tree=tree[parent]):
341
+ # return True
342
+
343
+ @classmethod
344
+ def is_child(cls, parent: TokenCategory, child: TokenCategory) -> bool:
345
+ """
346
+ Recursively check if `child` is in the subtree of `parent`. If `parent` is the same as `child`, return True.
347
+
348
+ Args:
349
+ parent (TokenCategory): The parent category.
350
+ child (TokenCategory): The category to check.
351
+
352
+ Returns:
353
+ bool: True if `child` is a descendant of `parent`, False otherwise.
354
+ """
355
+ if parent == child:
356
+ return True
357
+ return cls._is_child(parent, child, tree=cls.hierarchy)
358
+
359
+ @classmethod
360
+ def children(cls, parent: TokenCategory) -> Set[TokenCategory]:
361
+ """
362
+ Get the direct children of the parent category.
363
+
364
+ Args:
365
+ parent (TokenCategory): The parent category.
366
+
367
+ Returns:
368
+ Set[TokenCategory]: The list of children categories of the parent category.
369
+ """
370
+ return set(cls.hierarchy.get(parent, {}).keys())
371
+
372
+ @classmethod
373
+ def _nodes(cls, tree: _hierarchy_typing) -> Set[TokenCategory]:
374
+ """
375
+ Recursively get all nodes in the given hierarchy tree.
376
+ """
377
+ nodes = set(tree.keys())
378
+ for child in tree.values():
379
+ nodes.update(cls._nodes(child))
380
+ return nodes
381
+
382
+ @classmethod
383
+ def _find_subtree(cls, tree: '_hierarchy_typing', parent: TokenCategory) -> Optional['_hierarchy_typing']:
384
+ """
385
+ Recursively find the subtree for the given parent category.
386
+ """
387
+ if parent in tree:
388
+ return tree[parent] # Return subtree if parent is found at this level
389
+ for child, sub_tree in tree.items():
390
+ result = cls._find_subtree(sub_tree, parent)
391
+ if result is not None:
392
+ return result
393
+ return None # Return None if parent is not found. It won't happer never
394
+
395
+
396
+ @classmethod
397
+ def nodes(cls, parent: TokenCategory) -> Set[TokenCategory]:
398
+ """
399
+ Get the all nodes of the subtree of the parent category.
400
+
401
+ Args:
402
+ parent (TokenCategory): The parent category.
403
+
404
+ Returns:
405
+ List[TokenCategory]: The list of nodes of the subtree of the parent category.
406
+ """
407
+ subtree = cls._find_subtree(cls.hierarchy, parent)
408
+ return cls._nodes(subtree) if subtree is not None else set()
409
+
410
+ @classmethod
411
+ def valid(cls,
412
+ include: Optional[Set[TokenCategory]] = None,
413
+ exclude: Optional[Set[TokenCategory]] = None) -> Set[TokenCategory]:
414
+ """
415
+ Get the valid categories based on the include and exclude sets.
416
+
417
+ Args:
418
+ include (Optional[Set[TokenCategory]]): The set of categories to include. Defaults to None. \
419
+ If None, all categories are included.
420
+ exclude (Optional[Set[TokenCategory]]): The set of categories to exclude. Defaults to None. \
421
+ If None, no categories are excluded.
422
+
423
+ Returns (Set[TokenCategory]): The list of valid categories based on the include and exclude sets.
424
+ """
425
+ include = cls._validate_include(include)
426
+ exclude = cls._validate_exclude(exclude)
427
+
428
+ included_nodes = set.union(*[(cls.nodes(cat) | {cat}) for cat in include]) if len(include) > 0 else include
429
+ excluded_nodes = set.union(*[(cls.nodes(cat) | {cat}) for cat in exclude]) if len(exclude) > 0 else exclude
430
+ return included_nodes - excluded_nodes
431
+
432
+ @classmethod
433
+ def _leaves(cls, tree: '_hierarchy_typing') -> Set[TokenCategory]:
434
+ """
435
+ Recursively get all leaves (nodes without children) in the hierarchy tree.
436
+ """
437
+ if not tree:
438
+ return set()
439
+ leaves = {node for node, children in tree.items() if not children}
440
+ for node, children in tree.items():
441
+ leaves.update(cls._leaves(children))
442
+ return leaves
443
+
444
+ @classmethod
445
+ def leaves(cls, target: TokenCategory) -> Set[TokenCategory]:
446
+ """
447
+ Get the leaves of the subtree of the target category.
448
+
449
+ Args:
450
+ target (TokenCategory): The target category.
451
+
452
+ Returns (List[TokenCategory]): The list of leaf categories of the target category.
453
+ """
454
+ tree = cls._find_subtree(cls.hierarchy, target)
455
+ return cls._leaves(tree)
456
+
457
+
458
+ @classmethod
459
+ def _match(cls, category: TokenCategory, *,
460
+ include: Set[TokenCategory],
461
+ exclude: Set[TokenCategory]) -> bool:
462
+ """
463
+ Check if a category matches include/exclude criteria.
464
+ """
465
+ # Include the category itself along with its descendants.
466
+ target_nodes = cls.nodes(category) | {category}
467
+
468
+ valid_categories = cls.valid(include=include, exclude=exclude)
469
+
470
+ # Check if any node in the target set is in the valid categories.
471
+ return len(target_nodes & valid_categories) > 0
472
+
473
+ @classmethod
474
+ def _validate_include(cls, include: Optional[Set[TokenCategory]]) -> Set[TokenCategory]:
475
+ """
476
+ Validate the include set.
477
+ """
478
+ if include is None:
479
+ return cls.all()
480
+ if isinstance(include, (list, tuple)):
481
+ include = set(include)
482
+ elif not isinstance(include, set):
483
+ include = {include}
484
+ if not all(isinstance(cat, TokenCategory) for cat in include):
485
+ raise ValueError('Invalid category: include and exclude must be a set of TokenCategory.')
486
+ return include
487
+
488
+ @classmethod
489
+ def _validate_exclude(cls, exclude: Optional[Set[TokenCategory]]) -> Set[TokenCategory]:
490
+ """
491
+ Validate the exclude set.
492
+ """
493
+ if exclude is None:
494
+ return set()
495
+ if isinstance(exclude, (list, tuple)):
496
+ exclude = set(exclude)
497
+ elif not isinstance(exclude, set):
498
+ exclude = {exclude}
499
+ if not all(isinstance(cat, TokenCategory) for cat in exclude):
500
+ raise ValueError(f'Invalid category: category must be a {TokenCategory.__name__}.')
501
+ return exclude
502
+
503
+
504
+ @classmethod
505
+ def match(cls, category: TokenCategory, *,
506
+ include: Optional[Set[TokenCategory]] = None,
507
+ exclude: Optional[Set[TokenCategory]] = None) -> bool:
508
+ """
509
+ Check if the category matches the include and exclude sets.
510
+ If include is None, all categories are included. \
511
+ If exclude is None, no categories are excluded.
512
+
513
+ Args:
514
+ category (TokenCategory): The category to check.
515
+ include (Optional[Set[TokenCategory]]): The set of categories to include. Defaults to None. \
516
+ If None, all categories are included.
517
+ exclude (Optional[Set[TokenCategory]]): The set of categories to exclude. Defaults to None. \
518
+ If None, no categories are excluded.
519
+
520
+ Returns (bool): True if the category matches the include and exclude sets, False otherwise.
521
+
522
+ Examples:
523
+ >>> TokenCategoryHierarchyMapper.match(TokenCategory.NOTE, include={TokenCategory.NOTE_REST})
524
+ True
525
+ >>> TokenCategoryHierarchyMapper.match(TokenCategory.NOTE, include={TokenCategory.NOTE_REST}, exclude={TokenCategory.REST})
526
+ True
527
+ >>> TokenCategoryHierarchyMapper.match(TokenCategory.NOTE, include={TokenCategory.NOTE_REST}, exclude={TokenCategory.NOTE})
528
+ False
529
+ >>> TokenCategoryHierarchyMapper.match(TokenCategory.NOTE, include={TokenCategory.CORE}, exclude={TokenCategory.DURATION})
530
+ True
531
+ >>> TokenCategoryHierarchyMapper.match(TokenCategory.DURATION, include={TokenCategory.CORE}, exclude={TokenCategory.DURATION})
532
+ False
533
+ """
534
+ include = cls._validate_include(include)
535
+ exclude = cls._validate_exclude(exclude)
536
+
537
+ return cls._match(category, include=include, exclude=exclude)
538
+
539
+ @classmethod
540
+ def all(cls) -> Set[TokenCategory]:
541
+ """
542
+ Get all categories in the hierarchy.
543
+
544
+ Returns:
545
+ Set[TokenCategory]: The set of all categories in the hierarchy.
546
+ """
547
+ return cls._nodes(cls.hierarchy)
548
+
549
+ @classmethod
550
+ def tree(cls) -> str:
551
+ """
552
+ Return a string representation of the category hierarchy,
553
+ formatted similar to the output of the Unix 'tree' command.
554
+
555
+ Example output:
556
+ .
557
+ ├── STRUCTURAL
558
+ ├── CORE
559
+ │ ├── NOTE_REST
560
+ │ │ ├── DURATION
561
+ │ │ ├── NOTE
562
+ │ │ │ ├── PITCH
563
+ │ │ │ └── DECORATION
564
+ │ │ └── REST
565
+ │ ├── CHORD
566
+ │ └── EMPTY
567
+ ├── SIGNATURES
568
+ │ ├── CLEF
569
+ │ ├── TIME_SIGNATURE
570
+ │ ├── METER_SYMBOL
571
+ │ └── KEY_SIGNATURE
572
+ ├── ENGRAVED_SYMBOLS
573
+ ├── OTHER_CONTEXTUAL
574
+ ├── BARLINES
575
+ ├── COMMENTS
576
+ │ ├── FIELD_COMMENTS
577
+ │ └── LINE_COMMENTS
578
+ ├── DYNAMICS
579
+ ├── HARMONY
580
+ ...
581
+ """
582
+ def build_tree(tree: Dict[TokenCategory, '_hierarchy_typing'], prefix: str = "") -> [str]:
583
+ lines_buffer = []
584
+ items = list(tree.items())
585
+ count = len(items)
586
+ for index, (category, subtree) in enumerate(items):
587
+ connector = "└── " if index == count - 1 else "├── "
588
+ lines_buffer.append(prefix + connector + str(category))
589
+ extension = " " if index == count - 1 else "│ "
590
+ lines_buffer.extend(build_tree(subtree, prefix + extension))
591
+ return lines_buffer
592
+
593
+ lines = ["."]
594
+ lines.extend(build_tree(cls.hierarchy))
595
+ return "\n".join(lines)
596
+
597
+
598
+ class PitchRest:
599
+ """
600
+ Represents a name or a rest in a note.
601
+
602
+ The name is represented using the International Standard Organization (ISO) name notation.
603
+ The first line below the staff is the C4 in G clef. The above C is C5, the below C is C3, etc.
604
+
605
+ The Humdrum Kern format uses the following name representation:
606
+ 'c' = C4
607
+ 'cc' = C5
608
+ 'ccc' = C6
609
+ 'cccc' = C7
610
+
611
+ 'C' = C3
612
+ 'CC' = C2
613
+ 'CCC' = C1
614
+
615
+ The rests are represented by the letter 'r'. The rests do not have name.
616
+
617
+ This class do not limit the name ranges.
618
+
619
+
620
+ In the following example, the name is represented by the letter 'c'. The name of 'c' is C4, 'cc' is C5, 'ccc' is C6.
621
+ ```
622
+ **kern
623
+ *clefG2
624
+ 2c // C4
625
+ 2cc // C5
626
+ 2ccc // C6
627
+ 2C // C3
628
+ 2CC // C2
629
+ 2CCC // C1
630
+ *-
631
+ ```
632
+ """
633
+ C4_PITCH_LOWERCASE = 'c'
634
+ C4_OCATAVE = 4
635
+ C3_PITCH_UPPERCASE = 'C'
636
+ C3_OCATAVE = 3
637
+ REST_CHARACTER = 'r'
638
+
639
+ VALID_PITCHES = 'abcdefg' + 'ABCDEFG' + REST_CHARACTER
640
+
641
+ def __init__(self, raw_pitch: str):
642
+ """
643
+ Create a new PitchRest object.
644
+
645
+ Args:
646
+ raw_pitch (str): name representation in Humdrum Kern format
647
+
648
+ Examples:
649
+ >>> pitch_rest = PitchRest('c')
650
+ >>> pitch_rest = PitchRest('r')
651
+ >>> pitch_rest = PitchRest('DDD')
652
+ """
653
+ if raw_pitch is None or len(raw_pitch) == 0:
654
+ raise ValueError(f'Empty name: name can not be None or empty. But {raw_pitch} was provided.')
655
+
656
+ self.encoding = raw_pitch
657
+ self.pitch, self.octave = self.__parse_pitch_octave()
658
+
659
+ def __parse_pitch_octave(self) -> (str, int):
660
+ if self.encoding == PitchRest.REST_CHARACTER:
661
+ return PitchRest.REST_CHARACTER, None
662
+
663
+ if self.encoding.islower():
664
+ min_octave = PitchRest.C4_OCATAVE
665
+ octave = min_octave + (len(self.encoding) - 1)
666
+ pitch = self.encoding[0].lower()
667
+ return pitch, octave
668
+
669
+ if self.encoding.isupper():
670
+ max_octave = PitchRest.C3_OCATAVE
671
+ octave = max_octave - (len(self.encoding) - 1)
672
+ pitch = self.encoding[0].lower()
673
+ return pitch, octave
674
+
675
+ raise ValueError(f'Invalid name: name {self.encoding} is not a valid name representation.')
676
+
677
+ def is_rest(self) -> bool:
678
+ """
679
+ Check if the name is a rest.
680
+
681
+ Returns:
682
+ bool: True if the name is a rest, False otherwise.
683
+
684
+ Examples:
685
+ >>> pitch_rest = PitchRest('c')
686
+ >>> pitch_rest.is_rest()
687
+ False
688
+ >>> pitch_rest = PitchRest('r')
689
+ >>> pitch_rest.is_rest()
690
+ True
691
+ """
692
+ return self.octave is None
693
+
694
+ @staticmethod
695
+ def pitch_comparator(pitch_a: str, pitch_b: str) -> int:
696
+ """
697
+ Compare two pitches of the same octave.
698
+
699
+ The lower name is 'a'. So 'a' < 'b' < 'c' < 'd' < 'e' < 'f' < 'g'
700
+
701
+ Args:
702
+ pitch_a: One name of 'abcdefg'
703
+ pitch_b: Another name of 'abcdefg'
704
+
705
+ Returns:
706
+ -1 if pitch1 is lower than pitch2
707
+ 0 if pitch1 is equal to pitch2
708
+ 1 if pitch1 is higher than pitch2
709
+
710
+ Examples:
711
+ >>> PitchRest.pitch_comparator('c', 'c')
712
+ 0
713
+ >>> PitchRest.pitch_comparator('c', 'd')
714
+ -1
715
+ >>> PitchRest.pitch_comparator('d', 'c')
716
+ 1
717
+ """
718
+ if pitch_a < pitch_b:
719
+ return -1
720
+ if pitch_a > pitch_b:
721
+ return 1
722
+ return 0
723
+
724
+ def __str__(self):
725
+ return f'{self.encoding}'
726
+
727
+ def __repr__(self):
728
+ return f'[PitchRest: {self.encoding}, name={self.pitch}, octave={self.octave}]'
729
+
730
+ def __eq__(self, other: 'PitchRest') -> bool:
731
+ """
732
+ Compare two pitches and rests.
733
+
734
+ Args:
735
+ other (PitchRest): The other name to compare
736
+
737
+ Returns (bool):
738
+ True if the pitches are equal, False otherwise
739
+
740
+ Examples:
741
+ >>> pitch_rest = PitchRest('c')
742
+ >>> pitch_rest2 = PitchRest('c')
743
+ >>> pitch_rest == pitch_rest2
744
+ True
745
+ >>> pitch_rest = PitchRest('c')
746
+ >>> pitch_rest2 = PitchRest('ccc')
747
+ >>> pitch_rest == pitch_rest2
748
+ False
749
+ >>> pitch_rest = PitchRest('c')
750
+ >>> pitch_rest2 = PitchRest('r')
751
+ >>> pitch_rest == pitch_rest2
752
+ False
753
+ >>> pitch_rest = PitchRest('r')
754
+ >>> pitch_rest2 = PitchRest('r')
755
+ >>> pitch_rest == pitch_rest2
756
+ True
757
+
758
+ """
759
+ if not isinstance(other, PitchRest):
760
+ return False
761
+ if self.is_rest() and other.is_rest():
762
+ return True
763
+ if self.is_rest() or other.is_rest():
764
+ return False
765
+ return self.pitch == other.pitch and self.octave == other.octave
766
+
767
+ def __ne__(self, other: 'PitchRest') -> bool:
768
+ """
769
+ Compare two pitches and rests.
770
+ Args:
771
+ other (PitchRest): The other name to compare
772
+
773
+ Returns (bool):
774
+ True if the pitches are different, False otherwise
775
+
776
+ Examples:
777
+ >>> pitch_rest = PitchRest('c')
778
+ >>> pitch_rest2 = PitchRest('c')
779
+ >>> pitch_rest != pitch_rest2
780
+ False
781
+ >>> pitch_rest = PitchRest('c')
782
+ >>> pitch_rest2 = PitchRest('ccc')
783
+ >>> pitch_rest != pitch_rest2
784
+ True
785
+ >>> pitch_rest = PitchRest('c')
786
+ >>> pitch_rest2 = PitchRest('r')
787
+ >>> pitch_rest != pitch_rest2
788
+ True
789
+ >>> pitch_rest = PitchRest('r')
790
+ >>> pitch_rest2 = PitchRest('r')
791
+ >>> pitch_rest != pitch_rest2
792
+ False
793
+ """
794
+ return not self.__eq__(other)
795
+
796
+ def __gt__(self, other: 'PitchRest') -> bool:
797
+ """
798
+ Compare two pitches.
799
+
800
+ If any of the pitches is a rest, the comparison raise an exception.
801
+
802
+ Args:
803
+ other (PitchRest): The other name to compare
804
+
805
+ Returns (bool): True if this name is higher than the other, False otherwise
806
+
807
+ Examples:
808
+ >>> pitch_rest = PitchRest('c')
809
+ >>> pitch_rest2 = PitchRest('d')
810
+ >>> pitch_rest > pitch_rest2
811
+ False
812
+ >>> pitch_rest = PitchRest('c')
813
+ >>> pitch_rest2 = PitchRest('c')
814
+ >>> pitch_rest > pitch_rest2
815
+ False
816
+ >>> pitch_rest = PitchRest('c')
817
+ >>> pitch_rest2 = PitchRest('b')
818
+ >>> pitch_rest > pitch_rest2
819
+ True
820
+ >>> pitch_rest = PitchRest('r')
821
+ >>> pitch_rest2 = PitchRest('c')
822
+ >>> pitch_rest > pitch_rest2
823
+ Traceback (most recent call last):
824
+ ...
825
+ ValueError: ...
826
+ >>> pitch_rest = PitchRest('r')
827
+ >>> pitch_rest2 = PitchRest('r')
828
+ >>> pitch_rest > pitch_rest2
829
+ Traceback (most recent call last):
830
+ ValueError: ...
831
+
832
+
833
+ """
834
+ if self.is_rest() or other.is_rest():
835
+ raise ValueError(f'Invalid comparison: > operator can not be used to compare name of a rest.\n\
836
+ self={repr(self)} > other={repr(other)}')
837
+
838
+ if self.octave > other.octave:
839
+ return True
840
+ if self.octave == other.octave:
841
+ return PitchRest.pitch_comparator(self.pitch, other.pitch) > 0
842
+ return False
843
+
844
+ def __lt__(self, other: 'PitchRest') -> bool:
845
+ """
846
+ Compare two pitches.
847
+
848
+ If any of the pitches is a rest, the comparison raise an exception.
849
+
850
+ Args:
851
+ other: The other name to compare
852
+
853
+ Returns:
854
+ True if this name is lower than the other, False otherwise
855
+
856
+ Examples:
857
+ >>> pitch_rest = PitchRest('c')
858
+ >>> pitch_rest2 = PitchRest('d')
859
+ >>> pitch_rest < pitch_rest2
860
+ True
861
+ >>> pitch_rest = PitchRest('c')
862
+ >>> pitch_rest2 = PitchRest('c')
863
+ >>> pitch_rest < pitch_rest2
864
+ False
865
+ >>> pitch_rest = PitchRest('c')
866
+ >>> pitch_rest2 = PitchRest('b')
867
+ >>> pitch_rest < pitch_rest2
868
+ False
869
+ >>> pitch_rest = PitchRest('r')
870
+ >>> pitch_rest2 = PitchRest('c')
871
+ >>> pitch_rest < pitch_rest2
872
+ Traceback (most recent call last):
873
+ ...
874
+ ValueError: ...
875
+ >>> pitch_rest = PitchRest('r')
876
+ >>> pitch_rest2 = PitchRest('r')
877
+ >>> pitch_rest < pitch_rest2
878
+ Traceback (most recent call last):
879
+ ...
880
+ ValueError: ...
881
+
882
+ """
883
+ if self.is_rest() or other.is_rest():
884
+ raise ValueError(f'Invalid comparison: < operator can not be used to compare name of a rest.\n\
885
+ self={repr(self)} < other={repr(other)}')
886
+
887
+ if self.octave < other.octave:
888
+ return True
889
+ if self.octave == other.octave:
890
+ return PitchRest.pitch_comparator(self.pitch, other.pitch) < 0
891
+ return False
892
+
893
+ def __ge__(self, other: 'PitchRest') -> bool:
894
+ """
895
+ Compare two pitches. If any of the PitchRest is a rest, the comparison raise an exception.
896
+ Args:
897
+ other (PitchRest): The other name to compare
898
+
899
+ Returns (bool):
900
+ True if this name is higher or equal than the other, False otherwise
901
+
902
+ Examples:
903
+ >>> pitch_rest = PitchRest('c')
904
+ >>> pitch_rest2 = PitchRest('d')
905
+ >>> pitch_rest >= pitch_rest2
906
+ False
907
+ >>> pitch_rest = PitchRest('c')
908
+ >>> pitch_rest2 = PitchRest('c')
909
+ >>> pitch_rest >= pitch_rest2
910
+ True
911
+ >>> pitch_rest = PitchRest('c')
912
+ >>> pitch_rest2 = PitchRest('b')
913
+ >>> pitch_rest >= pitch_rest2
914
+ True
915
+ >>> pitch_rest = PitchRest('r')
916
+ >>> pitch_rest2 = PitchRest('c')
917
+ >>> pitch_rest >= pitch_rest2
918
+ Traceback (most recent call last):
919
+ ...
920
+ ValueError: ...
921
+ >>> pitch_rest = PitchRest('r')
922
+ >>> pitch_rest2 = PitchRest('r')
923
+ >>> pitch_rest >= pitch_rest2
924
+ Traceback (most recent call last):
925
+ ...
926
+ ValueError: ...
927
+
928
+
929
+ """
930
+ return self.__gt__(other) or self.__eq__(other)
931
+
932
+ def __le__(self, other: 'PitchRest') -> bool:
933
+ """
934
+ Compare two pitches. If any of the PitchRest is a rest, the comparison raise an exception.
935
+ Args:
936
+ other (PitchRest): The other name to compare
937
+
938
+ Returns (bool): True if this name is lower or equal than the other, False otherwise
939
+
940
+ Examples:
941
+ >>> pitch_rest = PitchRest('c')
942
+ >>> pitch_rest2 = PitchRest('d')
943
+ >>> pitch_rest <= pitch_rest2
944
+ True
945
+ >>> pitch_rest = PitchRest('c')
946
+ >>> pitch_rest2 = PitchRest('c')
947
+ >>> pitch_rest <= pitch_rest2
948
+ True
949
+ >>> pitch_rest = PitchRest('c')
950
+ >>> pitch_rest2 = PitchRest('b')
951
+ >>> pitch_rest <= pitch_rest2
952
+ False
953
+ >>> pitch_rest = PitchRest('r')
954
+ >>> pitch_rest2 = PitchRest('c')
955
+ >>> pitch_rest <= pitch_rest2
956
+ Traceback (most recent call last):
957
+ ...
958
+ ValueError: ...
959
+ >>> pitch_rest = PitchRest('r')
960
+ >>> pitch_rest2 = PitchRest('r')
961
+ >>> pitch_rest <= pitch_rest2
962
+ Traceback (most recent call last):
963
+ ...
964
+ ValueError: ...
965
+
966
+ """
967
+ return self.__lt__(other) or self.__eq__(other)
968
+
969
+
970
+ class Duration(ABC):
971
+ """
972
+ Represents the duration of a note or a rest.
973
+
974
+ The duration is represented using the Humdrum Kern format.
975
+ The duration is a number that represents the number of units of the duration.
976
+
977
+ The duration of a whole note is 1, half note is 2, quarter note is 4, eighth note is 8, etc.
978
+
979
+ The duration of a note is represented by a number. The duration of a rest is also represented by a number.
980
+
981
+ This class do not limit the duration ranges.
982
+
983
+ In the following example, the duration is represented by the number '2'.
984
+ ```
985
+ **kern
986
+ *clefG2
987
+ 2c // whole note
988
+ 4c // half note
989
+ 8c // quarter note
990
+ 16c // eighth note
991
+ *-
992
+ ```
993
+ """
994
+
995
+ def __init__(self, raw_duration):
996
+ self.encoding = str(raw_duration)
997
+
998
+ @abstractmethod
999
+ def modify(self, ratio: int):
1000
+ pass
1001
+
1002
+ @abstractmethod
1003
+ def __deepcopy__(self, memo=None):
1004
+ pass
1005
+
1006
+ @abstractmethod
1007
+ def __eq__(self, other):
1008
+ pass
1009
+
1010
+ @abstractmethod
1011
+ def __ne__(self, other):
1012
+ pass
1013
+
1014
+ @abstractmethod
1015
+ def __gt__(self, other):
1016
+ pass
1017
+
1018
+ @abstractmethod
1019
+ def __lt__(self, other):
1020
+ pass
1021
+
1022
+ @abstractmethod
1023
+ def __ge__(self, other):
1024
+ pass
1025
+
1026
+ @abstractmethod
1027
+ def __le__(self, other):
1028
+ pass
1029
+
1030
+ @abstractmethod
1031
+ def __str__(self):
1032
+ pass
1033
+
1034
+
1035
+ class DurationFactory:
1036
+ @staticmethod
1037
+ def create_duration(duration: str) -> Duration:
1038
+ return DurationClassical(int(duration))
1039
+
1040
+
1041
+ class DurationMensural(Duration):
1042
+ """
1043
+ Represents the duration in mensural notation of a note or a rest.
1044
+ """
1045
+
1046
+ def __init__(self, duration):
1047
+ super().__init__(duration)
1048
+ self.duration = duration
1049
+
1050
+ def __eq__(self, other):
1051
+ raise NotImplementedError()
1052
+
1053
+ def modify(self, ratio: int):
1054
+ raise NotImplementedError()
1055
+
1056
+ def __deepcopy__(self, memo=None):
1057
+ raise NotImplementedError()
1058
+
1059
+ def __gt__(self, other):
1060
+ raise NotImplementedError()
1061
+
1062
+ def __lt__(self, other):
1063
+ raise NotImplementedError()
1064
+
1065
+ def __le__(self, other):
1066
+ raise NotImplementedError()
1067
+
1068
+ def __str__(self):
1069
+ raise NotImplementedError()
1070
+
1071
+ def __ge__(self, other):
1072
+ raise NotImplementedError()
1073
+
1074
+ def __ne__(self, other):
1075
+ raise NotImplementedError()
1076
+
1077
+
1078
+ class DurationClassical(Duration):
1079
+ """
1080
+ Represents the duration in classical notation of a note or a rest.
1081
+ """
1082
+
1083
+ def __init__(self, duration: int):
1084
+ """
1085
+ Create a new Duration object.
1086
+
1087
+ Args:
1088
+ duration (str): duration representation in Humdrum Kern format
1089
+
1090
+ Examples:
1091
+ >>> duration = DurationClassical(2)
1092
+ True
1093
+ >>> duration = DurationClassical(4)
1094
+ True
1095
+ >>> duration = DurationClassical(32)
1096
+ True
1097
+ >>> duration = DurationClassical(1)
1098
+ True
1099
+ >>> duration = DurationClassical(0)
1100
+ False
1101
+ >>> duration = DurationClassical(-2)
1102
+ False
1103
+ >>> duration = DurationClassical(3)
1104
+ False
1105
+ >>> duration = DurationClassical(7)
1106
+ False
1107
+ """
1108
+ super().__init__(duration)
1109
+ if not DurationClassical.__is_valid_duration(duration):
1110
+ raise ValueError(f'Bad duration: {duration} was provided.')
1111
+
1112
+ self.duration = int(duration)
1113
+
1114
+ def modify(self, ratio: int):
1115
+ """
1116
+ Modify the duration of a note or a rest of the current object.
1117
+
1118
+ Args:
1119
+ ratio (int): The factor to modify the duration. The factor must be greater than 0.
1120
+
1121
+ Returns (DurationClassical): The new duration object with the modified duration.
1122
+
1123
+ Examples:
1124
+ >>> duration = DurationClassical(2)
1125
+ >>> new_duration = duration.modify(2)
1126
+ >>> new_duration.duration
1127
+ 4
1128
+ >>> duration = DurationClassical(2)
1129
+ >>> new_duration = duration.modify(0)
1130
+ Traceback (most recent call last):
1131
+ ...
1132
+ ValueError: Invalid factor provided: 0. The factor must be greater than 0.
1133
+ >>> duration = DurationClassical(2)
1134
+ >>> new_duration = duration.modify(-2)
1135
+ Traceback (most recent call last):
1136
+ ...
1137
+ ValueError: Invalid factor provided: -2. The factor must be greater than 0.
1138
+ """
1139
+ if not isinstance(ratio, int):
1140
+ raise ValueError(f'Invalid factor provided: {ratio}. The factor must be an integer.')
1141
+ if ratio <= 0:
1142
+ raise ValueError(f'Invalid factor provided: {ratio}. The factor must be greater than 0.')
1143
+
1144
+ return copy.deepcopy(DurationClassical(self.duration * ratio))
1145
+
1146
+ def __deepcopy__(self, memo=None):
1147
+ if memo is None:
1148
+ memo = {}
1149
+
1150
+ new_instance = DurationClassical(self.duration)
1151
+ new_instance.duration = self.duration
1152
+ return new_instance
1153
+
1154
+ def __str__(self):
1155
+ return f'{self.duration}'
1156
+
1157
+ def __eq__(self, other: 'DurationClassical') -> bool:
1158
+ """
1159
+ Compare two durations.
1160
+
1161
+ Args:
1162
+ other (DurationClassical): The other duration to compare
1163
+
1164
+ Returns (bool): True if the durations are equal, False otherwise
1165
+
1166
+
1167
+ Examples:
1168
+ >>> duration = DurationClassical(2)
1169
+ >>> duration2 = DurationClassical(2)
1170
+ >>> duration == duration2
1171
+ True
1172
+ >>> duration = DurationClassical(2)
1173
+ >>> duration2 = DurationClassical(4)
1174
+ >>> duration == duration2
1175
+ False
1176
+ """
1177
+ if not isinstance(other, DurationClassical):
1178
+ return False
1179
+ return self.duration == other.duration
1180
+
1181
+ def __ne__(self, other: 'DurationClassical') -> bool:
1182
+ """
1183
+ Compare two durations.
1184
+
1185
+ Args:
1186
+ other (DurationClassical): The other duration to compare
1187
+
1188
+ Returns (bool):
1189
+ True if the durations are different, False otherwise
1190
+
1191
+ Examples:
1192
+ >>> duration = DurationClassical(2)
1193
+ >>> duration2 = DurationClassical(2)
1194
+ >>> duration != duration2
1195
+ False
1196
+ >>> duration = DurationClassical(2)
1197
+ >>> duration2 = DurationClassical(4)
1198
+ >>> duration != duration2
1199
+ True
1200
+ """
1201
+ return not self.__eq__(other)
1202
+
1203
+ def __gt__(self, other: 'DurationClassical') -> bool:
1204
+ """
1205
+ Compare two durations.
1206
+
1207
+ Args:
1208
+ other: The other duration to compare
1209
+
1210
+ Returns (bool):
1211
+ True if this duration is higher than the other, False otherwise
1212
+
1213
+ Examples:
1214
+ >>> duration = DurationClassical(2)
1215
+ >>> duration2 = DurationClassical(4)
1216
+ >>> duration > duration2
1217
+ False
1218
+ >>> duration = DurationClassical(4)
1219
+ >>> duration2 = DurationClassical(2)
1220
+ >>> duration > duration2
1221
+ True
1222
+ >>> duration = DurationClassical(4)
1223
+ >>> duration2 = DurationClassical(4)
1224
+ >>> duration > duration2
1225
+ False
1226
+ """
1227
+ if not isinstance(other, DurationClassical):
1228
+ raise ValueError(f'Invalid comparison: > operator can not be used to compare duration with {type(other)}')
1229
+ return self.duration > other.duration
1230
+
1231
+ def __lt__(self, other: 'DurationClassical') -> bool:
1232
+ """
1233
+ Compare two durations.
1234
+
1235
+ Args:
1236
+ other (DurationClassical): The other duration to compare
1237
+
1238
+ Returns (bool):
1239
+ True if this duration is lower than the other, False otherwise
1240
+
1241
+ Examples:
1242
+ >>> duration = DurationClassical(2)
1243
+ >>> duration2 = DurationClassical(4)
1244
+ >>> duration < duration2
1245
+ True
1246
+ >>> duration = DurationClassical(4)
1247
+ >>> duration2 = DurationClassical(2)
1248
+ >>> duration < duration2
1249
+ False
1250
+ >>> duration = DurationClassical(4)
1251
+ >>> duration2 = DurationClassical(4)
1252
+ >>> duration < duration2
1253
+ False
1254
+ """
1255
+ if not isinstance(other, DurationClassical):
1256
+ raise ValueError(f'Invalid comparison: < operator can not be used to compare duration with {type(other)}')
1257
+ return self.duration < other.duration
1258
+
1259
+ def __ge__(self, other: 'DurationClassical') -> bool:
1260
+ """
1261
+ Compare two durations.
1262
+
1263
+ Args:
1264
+ other (DurationClassical): The other duration to compare
1265
+
1266
+ Returns (bool):
1267
+ True if this duration is higher or equal than the other, False otherwise
1268
+
1269
+ Examples:
1270
+ >>> duration = DurationClassical(2)
1271
+ >>> duration2 = DurationClassical(4)
1272
+ >>> duration >= duration2
1273
+ False
1274
+ >>> duration = DurationClassical(4)
1275
+ >>> duration2 = DurationClassical(2)
1276
+ >>> duration >= duration2
1277
+ True
1278
+ >>> duration = DurationClassical(4)
1279
+ >>> duration2 = DurationClassical(4)
1280
+ >>> duration >= duration2
1281
+ True
1282
+ """
1283
+ return self.__gt__(other) or self.__eq__(other)
1284
+
1285
+ def __le__(self, other: 'DurationClassical') -> bool:
1286
+ """
1287
+ Compare two durations.
1288
+
1289
+ Args:
1290
+ other (DurationClassical): The other duration to compare
1291
+
1292
+ Returns:
1293
+ True if this duration is lower or equal than the other, False otherwise
1294
+
1295
+ Examples:
1296
+ >>> duration = DurationClassical(2)
1297
+ >>> duration2 = DurationClassical(4)
1298
+ >>> duration <= duration2
1299
+ True
1300
+ >>> duration = DurationClassical(4)
1301
+ >>> duration2 = DurationClassical(2)
1302
+ >>> duration <= duration2
1303
+ False
1304
+ >>> duration = DurationClassical(4)
1305
+ >>> duration2 = DurationClassical(4)
1306
+ >>> duration <= duration2
1307
+ True
1308
+ """
1309
+ return self.__lt__(other) or self.__eq__(other)
1310
+
1311
+ @classmethod
1312
+ def __is_valid_duration(cls, duration: int) -> bool:
1313
+ try:
1314
+ duration = int(duration)
1315
+ if duration is None or duration <= 0:
1316
+ return False
1317
+
1318
+ return duration > 0 and (duration % 2 == 0 or duration == 1)
1319
+ except ValueError:
1320
+ return False
1321
+
1322
+
1323
+ class Subtoken:
1324
+ """
1325
+ Subtoken class. Thhe subtokens are the smallest units of categories. ComplexToken objects are composed of subtokens.
1326
+
1327
+ Attributes:
1328
+ encoding: The complete unprocessed encoding
1329
+ category: The subtoken category, one of SubTokenCategory
1330
+ """
1331
+ DECORATION = None
1332
+
1333
+ def __init__(self, encoding: str, category: TokenCategory):
1334
+ """
1335
+ Subtoken constructor
1336
+
1337
+ Args:
1338
+ encoding (str): The complete unprocessed encoding
1339
+ category (TokenCategory): The subtoken category. \
1340
+ It should be a child of the main 'TokenCategory' in the hierarchy.
1341
+
1342
+ """
1343
+ self.encoding = encoding
1344
+ self.category = category
1345
+
1346
+ def __str__(self):
1347
+ """
1348
+ Returns the string representation of the subtoken.
1349
+
1350
+ Returns (str): The string representation of the subtoken.
1351
+ """
1352
+ return self.encoding
1353
+
1354
+ def __eq__(self, other):
1355
+ """
1356
+ Compare two subtokens.
1357
+
1358
+ Args:
1359
+ other (Subtoken): The other subtoken to compare.
1360
+ Returns (bool): True if the subtokens are equal, False otherwise.
1361
+ """
1362
+ if not isinstance(other, Subtoken):
1363
+ return False
1364
+ return self.encoding == other.encoding and self.category == other.category
1365
+
1366
+ def __ne__(self, other):
1367
+ """
1368
+ Compare two subtokens.
1369
+
1370
+ Args:
1371
+ other (Subtoken): The other subtoken to compare.
1372
+ Returns (bool): True if the subtokens are different, False otherwise.
1373
+ """
1374
+ return not self.__eq__(other)
1375
+
1376
+ def __hash__(self):
1377
+ """
1378
+ Returns the hash of the subtoken.
1379
+
1380
+ Returns (int): The hash of the subtoken.
1381
+ """
1382
+ return hash((self.encoding, self.category))
1383
+
1384
+ class AbstractToken(ABC):
1385
+ """
1386
+ An abstract base class representing a token.
1387
+
1388
+ This class serves as a blueprint for creating various types of tokens, which are
1389
+ categorized based on their TokenCategory.
1390
+
1391
+ Attributes:
1392
+ encoding (str): The original representation of the token.
1393
+ category (TokenCategory): The category of the token.
1394
+ hidden (bool): A flag indicating whether the token is hidden. Defaults to False.
1395
+ """
1396
+
1397
+ def __init__(self, encoding: str, category: TokenCategory):
1398
+ """
1399
+ AbstractToken constructor
1400
+
1401
+ Args:
1402
+ encoding (str): The original representation of the token.
1403
+ category (TokenCategory): The category of the token.
1404
+ """
1405
+ self.encoding = encoding
1406
+ self.category = category
1407
+ self.hidden = False
1408
+
1409
+ @abstractmethod
1410
+ def export(self, **kwargs) -> str:
1411
+ """
1412
+ Exports the token.
1413
+
1414
+ Keyword Arguments:
1415
+ filter_categories (Optional[Callable[[TokenCategory], bool]]): A function that takes a TokenCategory and returns a boolean
1416
+ indicating whether the token should be included in the export. If provided, only tokens for which the
1417
+ function returns True will be exported. Defaults to None. If None, all tokens will be exported.
1418
+
1419
+ Returns:
1420
+ str: The encoded token representation, potentially filtered if a filter_categories function is provided.
1421
+
1422
+ Examples:
1423
+ >>> token = AbstractToken('*clefF4', TokenCategory.SIGNATURES)
1424
+ >>> token.export()
1425
+ '*clefF4'
1426
+ >>> token.export(filter_categories=lambda cat: cat in {TokenCategory.SIGNATURES, TokenCategory.SIGNATURES.DURATION})
1427
+ '*clefF4'
1428
+ """
1429
+ pass
1430
+
1431
+
1432
+ def __str__(self):
1433
+ """
1434
+ Returns the string representation of the token.
1435
+
1436
+ Returns (str): The string representation of the token without processing.
1437
+ """
1438
+ return self.export()
1439
+
1440
+ def __eq__(self, other):
1441
+ """
1442
+ Compare two tokens.
1443
+
1444
+ Args:
1445
+ other (AbstractToken): The other token to compare.
1446
+ Returns (bool): True if the tokens are equal, False otherwise.
1447
+ """
1448
+ if not isinstance(other, AbstractToken):
1449
+ return False
1450
+ return self.encoding == other.encoding and self.category == other.category
1451
+
1452
+ def __ne__(self, other):
1453
+ """
1454
+ Compare two tokens.
1455
+
1456
+ Args:
1457
+ other (AbstractToken): The other token to compare.
1458
+ Returns (bool): True if the tokens are different, False otherwise.
1459
+ """
1460
+ return not self.__eq__(other)
1461
+
1462
+ def __hash__(self):
1463
+ """
1464
+ Returns the hash of the token.
1465
+
1466
+ Returns (int): The hash of the token.
1467
+ """
1468
+ return hash((self.export(), self.category))
1469
+
1470
+
1471
+ class Token(AbstractToken, ABC):
1472
+ """
1473
+ Abstract Token class.
1474
+ """
1475
+
1476
+ def __init__(self, encoding, category):
1477
+ super().__init__(encoding, category)
1478
+
1479
+
1480
+ class SimpleToken(Token):
1481
+ """
1482
+ SimpleToken class.
1483
+ """
1484
+
1485
+ def __init__(self, encoding, category):
1486
+ super().__init__(encoding, category)
1487
+
1488
+ def export(self, **kwargs) -> str:
1489
+ """
1490
+ Exports the token.
1491
+
1492
+ Args:
1493
+ **kwargs: 'filter_categories' (Optional[Callable[[TokenCategory], bool]]): It is ignored in this class.
1494
+
1495
+ Returns (str): The encoded token representation.
1496
+ """
1497
+ return self.encoding
1498
+
1499
+
1500
+ class ErrorToken(SimpleToken):
1501
+ """
1502
+ Used to wrap tokens that have not been parsed.
1503
+ """
1504
+
1505
+ def __init__(
1506
+ self,
1507
+ encoding: str,
1508
+ line: int,
1509
+ error: str
1510
+ ):
1511
+ """
1512
+ ErrorToken constructor
1513
+
1514
+ Args:
1515
+ encoding (str): The original representation of the token.
1516
+ line (int): The line number of the token in the score.
1517
+ error (str): The error message thrown by the parser.
1518
+ """
1519
+ super().__init__(encoding, TokenCategory.ERROR)
1520
+ self.error = error
1521
+ self.line = line
1522
+
1523
+ def export(self, **kwargs) -> str:
1524
+ """
1525
+ Exports the error token.
1526
+
1527
+ Returns (str): A string representation of the error token.
1528
+ """
1529
+ # return ERROR_TOKEN
1530
+ return self.encoding # TODO: add a constant for the error token
1531
+
1532
+ def __str__(self):
1533
+ """
1534
+ Information about the error token.
1535
+
1536
+ Returns (str) The information about the error token.
1537
+ """
1538
+ return f'Error token found at line {self.line} with encoding "{self.encoding}". Description: {self.error}'
1539
+
1540
+
1541
+ class MetacommentToken(SimpleToken):
1542
+ """
1543
+ MetacommentToken class stores the metacomments of the score.
1544
+ Usually these are comments starting with `!!`.
1545
+
1546
+ """
1547
+
1548
+ def __init__(self, encoding: str):
1549
+ """
1550
+ Constructor for the MetacommentToken class.
1551
+
1552
+ Args:
1553
+ encoding (str): The original representation of the token.
1554
+ """
1555
+ super().__init__(encoding, TokenCategory.LINE_COMMENTS)
1556
+
1557
+
1558
+ class InstrumentToken(SimpleToken):
1559
+ """
1560
+ InstrumentToken class stores the instruments of the score.
1561
+
1562
+ These tokens usually look like `*I"Organo`.
1563
+ """
1564
+
1565
+ def __init__(self, encoding: str):
1566
+ """
1567
+ Constructor for the InstrumentToken
1568
+
1569
+ Args:
1570
+ encoding:
1571
+ """
1572
+ super().__init__(encoding, TokenCategory.INSTRUMENTS)
1573
+
1574
+
1575
+ class FieldCommentToken(SimpleToken):
1576
+ """
1577
+ FieldCommentToken class stores the metacomments of the score.
1578
+ Usually these are comments starting with `!!!`.
1579
+
1580
+ """
1581
+
1582
+ def __init__(self, encoding):
1583
+ super().__init__(encoding, TokenCategory.FIELD_COMMENTS)
1584
+
1585
+
1586
+ class HeaderToken(SimpleToken):
1587
+ """
1588
+ HeaderTokens class.
1589
+ """
1590
+
1591
+ def __init__(self, encoding, spine_id: int):
1592
+ """
1593
+ Constructor for the HeaderToken class.
1594
+
1595
+ Args:
1596
+ encoding (str): The original representation of the token.
1597
+ spine_id (int): The spine id of the token. The spine id is used to identify the token in the score.\
1598
+ The spine_id starts from 0 and increases by 1 for each new spine like the following example:
1599
+ **kern **kern **kern **dyn **text
1600
+ 0 1 2 3 4
1601
+ """
1602
+ super().__init__(encoding, TokenCategory.HEADER)
1603
+ self.spine_id = spine_id
1604
+
1605
+ def export(self, **kwargs) -> str:
1606
+ return self.encoding
1607
+
1608
+
1609
+ class SpineOperationToken(SimpleToken):
1610
+ """
1611
+ SpineOperationToken class.
1612
+
1613
+ This token represents different operations in the Humdrum kern encoding.
1614
+ These are the available operations:
1615
+ - `*-`: spine-path terminator.
1616
+ - `*`: null interpretation.
1617
+ - `*+`: add spines.
1618
+ - `*^`: split spines.
1619
+ - `*x`: exchange spines.
1620
+
1621
+ Attributes:
1622
+ cancelled_at_stage (int): The stage at which the operation was cancelled. Defaults to None.
1623
+ """
1624
+
1625
+ def __init__(self, encoding):
1626
+ super().__init__(encoding, TokenCategory.SPINE_OPERATION)
1627
+ self.cancelled_at_stage = None
1628
+
1629
+ def is_cancelled_at(self, stage) -> bool:
1630
+ """
1631
+ Checks if the operation was cancelled at the given stage.
1632
+
1633
+ Args:
1634
+ stage (int): The stage at which the operation was cancelled.
1635
+
1636
+ Returns:
1637
+ bool: True if the operation was cancelled at the given stage, False otherwise.
1638
+ """
1639
+ if self.cancelled_at_stage is None:
1640
+ return False
1641
+ else:
1642
+ return self.cancelled_at_stage < stage
1643
+
1644
+
1645
+ class BarToken(SimpleToken):
1646
+ """
1647
+ BarToken class.
1648
+ """
1649
+
1650
+ def __init__(self, encoding):
1651
+ super().__init__(encoding, TokenCategory.BARLINES)
1652
+
1653
+
1654
+ class SignatureToken(SimpleToken):
1655
+ """
1656
+ SignatureToken class for all signature tokens. It will be overridden by more specific classes.
1657
+ """
1658
+
1659
+ def __init__(self, encoding, category=TokenCategory.SIGNATURES):
1660
+ super().__init__(encoding, category)
1661
+
1662
+
1663
+ class ClefToken(SignatureToken):
1664
+ """
1665
+ ClefToken class.
1666
+ """
1667
+
1668
+ def __init__(self, encoding):
1669
+ super().__init__(encoding, TokenCategory.CLEF)
1670
+
1671
+
1672
+ class TimeSignatureToken(SignatureToken):
1673
+ """
1674
+ TimeSignatureToken class.
1675
+ """
1676
+
1677
+ def __init__(self, encoding):
1678
+ super().__init__(encoding, TokenCategory.TIME_SIGNATURE)
1679
+
1680
+
1681
+ class MeterSymbolToken(SignatureToken):
1682
+ """
1683
+ MeterSymbolToken class.
1684
+ """
1685
+
1686
+ def __init__(self, encoding):
1687
+ super().__init__(encoding, TokenCategory.METER_SYMBOL)
1688
+
1689
+
1690
+ class KeySignatureToken(SignatureToken):
1691
+ """
1692
+ KeySignatureToken class.
1693
+ """
1694
+
1695
+ def __init__(self, encoding):
1696
+ super().__init__(encoding, TokenCategory.KEY_SIGNATURE)
1697
+
1698
+
1699
+ class KeyToken(SignatureToken):
1700
+ """
1701
+ KeyToken class.
1702
+ """
1703
+
1704
+ def __init__(self, encoding):
1705
+ super().__init__(encoding, TokenCategory.KEY_TOKEN)
1706
+
1707
+
1708
+ class ComplexToken(Token, ABC):
1709
+ """
1710
+ Abstract ComplexToken class. This abstract class ensures that the subclasses implement the export method using\
1711
+ the 'filter_categories' parameter to filter the subtokens.
1712
+
1713
+ Passing the argument 'filter_categories' by **kwargs don't break the compatibility with parent classes.
1714
+
1715
+ Here we're trying to get the Liskov substitution principle done...
1716
+ """
1717
+ def __init__(self, encoding: str, category: TokenCategory):
1718
+ """
1719
+ Constructor for the ComplexToken
1720
+
1721
+ Args:
1722
+ encoding (str): The original representation of the token.
1723
+ category (TokenCategory) : The category of the token.
1724
+ """
1725
+ super().__init__(encoding, category)
1726
+
1727
+ @abstractmethod
1728
+ def export(self, **kwargs) -> str:
1729
+ """
1730
+ Exports the token.
1731
+
1732
+ Keyword Arguments:
1733
+ filter_categories (Optional[Callable[[TokenCategory], bool]]): A function that takes a TokenCategory and returns a boolean
1734
+ indicating whether the token should be included in the export. If provided, only tokens for which the
1735
+ function returns True will be exported. Defaults to None. If None, all tokens will be exported.
1736
+
1737
+ Returns (str): The exported token.
1738
+ """
1739
+ pass
1740
+
1741
+
1742
+ class CompoundToken(ComplexToken):
1743
+ def __init__(self, encoding: str, category: TokenCategory, subtokens: List[Subtoken]):
1744
+ """
1745
+ Args:
1746
+ encoding (str): The complete unprocessed encoding
1747
+ category (TokenCategory): The token category, one of 'TokenCategory'
1748
+ subtokens (List[Subtoken]): The individual elements of the token. Also of type 'TokenCategory' but \
1749
+ in the hierarchy they must be children of the current token.
1750
+ """
1751
+ super().__init__(encoding, category)
1752
+
1753
+ for subtoken in subtokens:
1754
+ if not isinstance(subtoken, Subtoken):
1755
+ raise ValueError(f'All subtokens must be instances of Subtoken. Found {type(subtoken)}')
1756
+
1757
+ self.subtokens = subtokens
1758
+
1759
+ def export(self, **kwargs) -> str:
1760
+ """
1761
+ Exports the token.
1762
+
1763
+ Keyword Arguments:
1764
+ filter_categories (Optional[Callable[[TokenCategory], bool]]): A function that takes a TokenCategory and returns a boolean
1765
+ indicating whether the token should be included in the export. If provided, only tokens for which the
1766
+ function returns True will be exported. Defaults to None. If None, all tokens will be exported.
1767
+
1768
+ Returns (str): The exported token.
1769
+ """
1770
+ filter_categories_fn = kwargs.get('filter_categories', None)
1771
+ parts = []
1772
+ for subtoken in self.subtokens:
1773
+ # Only export the subtoken if it passes the filter_categories (if provided)
1774
+ if filter_categories_fn is None or filter_categories_fn(subtoken.category):
1775
+ # parts.append(subtoken.export(**kwargs)) in the future when SubTokens will be Tokens
1776
+ parts.append(subtoken.encoding)
1777
+ return TOKEN_SEPARATOR.join(parts) if len(parts) > 0 else EMPTY_TOKEN
1778
+
1779
+
1780
+ class NoteRestToken(ComplexToken):
1781
+ """
1782
+ NoteRestToken class.
1783
+
1784
+ Attributes:
1785
+ pitch_duration_subtokens (list): The subtokens for the pitch and duration
1786
+ decoration_subtokens (list): The subtokens for the decorations
1787
+ """
1788
+
1789
+ def __init__(
1790
+ self,
1791
+ encoding: str,
1792
+ pitch_duration_subtokens: List[Subtoken],
1793
+ decoration_subtokens: List[Subtoken]
1794
+ ):
1795
+ """
1796
+ NoteRestToken constructor.
1797
+
1798
+ Args:
1799
+ encoding (str): The complete unprocessed encoding
1800
+ pitch_duration_subtokens (List[Subtoken])y: The subtokens for the pitch and duration
1801
+ decoration_subtokens (List[Subtoken]): The subtokens for the decorations. Individual elements of the token, of type Subtoken
1802
+ """
1803
+ super().__init__(encoding, TokenCategory.NOTE_REST)
1804
+ if not pitch_duration_subtokens or len(pitch_duration_subtokens) == 0:
1805
+ raise ValueError('Empty name-duration subtokens')
1806
+
1807
+ for subtoken in pitch_duration_subtokens:
1808
+ if not isinstance(subtoken, Subtoken):
1809
+ raise ValueError(f'All pitch-duration subtokens must be instances of Subtoken. Found {type(subtoken)}')
1810
+ for subtoken in decoration_subtokens:
1811
+ if not isinstance(subtoken, Subtoken):
1812
+ raise ValueError(f'All decoration subtokens must be instances of Subtoken. Found {type(subtoken)}')
1813
+
1814
+ self.pitch_duration_subtokens = pitch_duration_subtokens
1815
+ self.decoration_subtokens = decoration_subtokens
1816
+
1817
+ def export(self, **kwargs) -> str:
1818
+ """
1819
+ Exports the token.
1820
+
1821
+ Keyword Arguments:
1822
+ filter_categories (Optional[Callable[[TokenCategory], bool]]): A function that takes a TokenCategory and returns a boolean
1823
+ indicating whether the token should be included in the export. If provided, only tokens for which the
1824
+ function returns True will be exported. Defaults to None. If None, all tokens will be exported.
1825
+
1826
+ Returns (str): The exported token.
1827
+
1828
+ """
1829
+ filter_categories_fn = kwargs.get('filter_categories', None)
1830
+
1831
+ # Filter subcategories
1832
+ pitch_duration_tokens = {
1833
+ subtoken for subtoken in self.pitch_duration_subtokens
1834
+ if filter_categories_fn is None or filter_categories_fn(subtoken.category)
1835
+ }
1836
+ decoration_tokens = {
1837
+ subtoken for subtoken in self.decoration_subtokens
1838
+ if filter_categories_fn is None or filter_categories_fn(subtoken.category)
1839
+ }
1840
+ pitch_duration_tokens_sorted = sorted(pitch_duration_tokens, key=lambda t: (t.category.value, t.encoding))
1841
+ decoration_tokens_sorted = sorted(decoration_tokens, key=lambda t: (t.category.value, t.encoding))
1842
+
1843
+ # Join the sorted subtokens
1844
+ pitch_duration_part = TOKEN_SEPARATOR.join([subtoken.encoding for subtoken in pitch_duration_tokens_sorted])
1845
+ decoration_part = DECORATION_SEPARATOR.join([subtoken.encoding for subtoken in decoration_tokens_sorted])
1846
+
1847
+ result = pitch_duration_part
1848
+ if len(decoration_part):
1849
+ result += DECORATION_SEPARATOR + decoration_part
1850
+
1851
+ return result if len(result) > 0 else EMPTY_TOKEN
1852
+
1853
+
1854
+ class ChordToken(SimpleToken):
1855
+ """
1856
+ ChordToken class.
1857
+
1858
+ It contains a list of compound tokens
1859
+ """
1860
+
1861
+ def __init__(self,
1862
+ encoding: str,
1863
+ category: TokenCategory,
1864
+ notes_tokens: Sequence[Token]
1865
+ ):
1866
+ """
1867
+ ChordToken constructor.
1868
+
1869
+ Args:
1870
+ encoding (str): The complete unprocessed encoding
1871
+ category (TokenCategory): The token category, one of TokenCategory
1872
+ notes_tokens (Sequence[Token]): The subtokens for the notes. Individual elements of the token, of type token
1873
+ """
1874
+ super().__init__(encoding, category)
1875
+ self.notes_tokens = notes_tokens
1876
+
1877
+ def export(self, **kwargs) -> str:
1878
+ result = ''
1879
+ for note_token in self.notes_tokens:
1880
+ if len(result) > 0:
1881
+ result += ' '
1882
+
1883
+ result += note_token.export()
1884
+
1885
+ return result
1886
+
1887
+
1888
+ class BoundingBox:
1889
+ """
1890
+ BoundingBox class.
1891
+
1892
+ It contains the coordinates of the score bounding box. Useful for full-page tasks.
1893
+
1894
+ Attributes:
1895
+ from_x (int): The x coordinate of the top left corner
1896
+ from_y (int): The y coordinate of the top left corner
1897
+ to_x (int): The x coordinate of the bottom right corner
1898
+ to_y (int): The y coordinate of the bottom right corner
1899
+ """
1900
+
1901
+ def __init__(self, x, y, w, h):
1902
+ """
1903
+ BoundingBox constructor.
1904
+
1905
+ Args:
1906
+ x (int): The x coordinate of the top left corner
1907
+ y (int): The y coordinate of the top left corner
1908
+ w (int): The width
1909
+ h (int): The height
1910
+ """
1911
+ self.from_x = x
1912
+ self.from_y = y
1913
+ self.to_x = x + w
1914
+ self.to_y = y + h
1915
+
1916
+ def w(self) -> int:
1917
+ """
1918
+ Returns the width of the box
1919
+
1920
+ Returns:
1921
+ int: The width of the box
1922
+ """
1923
+ return self.to_x - self.from_x
1924
+
1925
+ def h(self) -> int:
1926
+ """
1927
+ Returns the height of the box
1928
+
1929
+ Returns:
1930
+ int: The height of the box
1931
+ return self.to_y - self.from_y
1932
+ """
1933
+ return self.to_y - self.from_y
1934
+
1935
+ def extend(self, bounding_box) -> None:
1936
+ """
1937
+ Extends the bounding box. Modify the current object.
1938
+
1939
+ Args:
1940
+ bounding_box (BoundingBox): The bounding box to extend
1941
+
1942
+ Returns:
1943
+ None
1944
+ """
1945
+ self.from_x = min(self.from_x, bounding_box.from_x)
1946
+ self.from_y = min(self.from_y, bounding_box.from_y)
1947
+ self.to_x = max(self.to_x, bounding_box.to_x)
1948
+ self.to_y = max(self.to_y, bounding_box.to_y)
1949
+
1950
+ def __str__(self) -> str:
1951
+ """
1952
+ Returns a string representation of the bounding box
1953
+
1954
+ Returns (str): The string representation of the bounding box
1955
+ """
1956
+ return f'(x={self.from_x}, y={self.from_y}, w={self.w()}, h={self.h()})'
1957
+
1958
+ def xywh(self) -> str:
1959
+ """
1960
+ Returns a string representation of the bounding box.
1961
+
1962
+ Returns:
1963
+ str: The string representation of the bounding box
1964
+ """
1965
+ return f'{self.from_x},{self.from_y},{self.w()},{self.h()}'
1966
+
1967
+
1968
+ class BoundingBoxToken(Token):
1969
+ """
1970
+ BoundingBoxToken class.
1971
+
1972
+ It contains the coordinates of the score bounding box. Useful for full-page tasks.
1973
+
1974
+ Attributes:
1975
+ encoding (str): The complete unprocessed encoding
1976
+ page_number (int): The page number
1977
+ bounding_box (BoundingBox): The bounding box
1978
+ """
1979
+
1980
+ def __init__(
1981
+ self,
1982
+ encoding: str,
1983
+ page_number: int,
1984
+ bounding_box: BoundingBox
1985
+ ):
1986
+ """
1987
+ BoundingBoxToken constructor.
1988
+
1989
+ Args:
1990
+ encoding (str): The complete unprocessed encoding
1991
+ page_number (int): The page number
1992
+ bounding_box (BoundingBox): The bounding box
1993
+ """
1994
+ super().__init__(encoding, TokenCategory.BOUNDING_BOXES)
1995
+ self.page_number = page_number
1996
+ self.bounding_box = bounding_box
1997
+
1998
+ def export(self, **kwargs) -> str:
1999
+ return self.encoding
2000
+
2001
+
2002
+ class MHXMToken(Token):
2003
+ """
2004
+ MHXMToken class.
2005
+ """
2006
+ def __init__(self, encoding):
2007
+ super().__init__(encoding, TokenCategory.MHXM)
2008
+
2009
+ # TODO: Implement constructor
2010
+ def export(self, **kwargs) -> str:
2011
+ return self.encoding