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.
- kernpy/__init__.py +215 -0
- kernpy/__main__.py +217 -0
- kernpy/core/__init__.py +119 -0
- kernpy/core/_io.py +48 -0
- kernpy/core/base_antlr_importer.py +61 -0
- kernpy/core/base_antlr_spine_parser_listener.py +196 -0
- kernpy/core/basic_spine_importer.py +43 -0
- kernpy/core/document.py +965 -0
- kernpy/core/dyn_importer.py +30 -0
- kernpy/core/dynam_spine_importer.py +42 -0
- kernpy/core/error_listener.py +51 -0
- kernpy/core/exporter.py +535 -0
- kernpy/core/fing_spine_importer.py +42 -0
- kernpy/core/generated/kernSpineLexer.interp +444 -0
- kernpy/core/generated/kernSpineLexer.py +535 -0
- kernpy/core/generated/kernSpineLexer.tokens +236 -0
- kernpy/core/generated/kernSpineParser.interp +425 -0
- kernpy/core/generated/kernSpineParser.py +9954 -0
- kernpy/core/generated/kernSpineParser.tokens +236 -0
- kernpy/core/generated/kernSpineParserListener.py +1200 -0
- kernpy/core/generated/kernSpineParserVisitor.py +673 -0
- kernpy/core/generic.py +426 -0
- kernpy/core/gkern.py +526 -0
- kernpy/core/graphviz_exporter.py +89 -0
- kernpy/core/harm_spine_importer.py +41 -0
- kernpy/core/import_humdrum_old.py +853 -0
- kernpy/core/importer.py +285 -0
- kernpy/core/importer_factory.py +43 -0
- kernpy/core/kern_spine_importer.py +73 -0
- kernpy/core/mens_spine_importer.py +23 -0
- kernpy/core/mhxm_spine_importer.py +44 -0
- kernpy/core/pitch_models.py +338 -0
- kernpy/core/root_spine_importer.py +58 -0
- kernpy/core/spine_importer.py +45 -0
- kernpy/core/text_spine_importer.py +43 -0
- kernpy/core/tokenizers.py +239 -0
- kernpy/core/tokens.py +2011 -0
- kernpy/core/transposer.py +300 -0
- kernpy/io/__init__.py +14 -0
- kernpy/io/public.py +355 -0
- kernpy/polish_scores/__init__.py +13 -0
- kernpy/polish_scores/download_polish_dataset.py +357 -0
- kernpy/polish_scores/iiif.py +47 -0
- kernpy/test_grammar.sh +22 -0
- kernpy/util/__init__.py +14 -0
- kernpy/util/helpers.py +55 -0
- kernpy/util/store_cache.py +35 -0
- kernpy/visualize_analysis.sh +23 -0
- kernpy-1.0.0.dist-info/METADATA +501 -0
- kernpy-1.0.0.dist-info/RECORD +51 -0
- {kernpy-0.0.1.dist-info → kernpy-1.0.0.dist-info}/WHEEL +1 -2
- kernpy/example.py +0 -0
- kernpy-0.0.1.dist-info/LICENSE +0 -19
- kernpy-0.0.1.dist-info/METADATA +0 -19
- kernpy-0.0.1.dist-info/RECORD +0 -7
- kernpy-0.0.1.dist-info/top_level.txt +0 -1
kernpy/core/gkern.py
ADDED
@@ -0,0 +1,526 @@
|
|
1
|
+
"""
|
2
|
+
|
3
|
+
This module is responsible for generating the Graphic **kern encoding.
|
4
|
+
|
5
|
+
The Graphic **kern encoding (**gkern) is an agnostify **kern encoding. It keeps the original **kern encoding structure \
|
6
|
+
but replaces the pitches with its graphic representation. So the E pitch in G Clef will use the same graphic \
|
7
|
+
representation as the C pitch in C in 1st Clef or the A in F in 4th Clef.
|
8
|
+
"""
|
9
|
+
|
10
|
+
from __future__ import annotations
|
11
|
+
|
12
|
+
from abc import ABC, abstractmethod
|
13
|
+
from enum import Enum
|
14
|
+
from typing import Dict
|
15
|
+
|
16
|
+
from .tokens import (
|
17
|
+
GRAPHIC_TOKEN_SEPARATOR, ClefToken, Token, TokenCategory
|
18
|
+
)
|
19
|
+
|
20
|
+
from .transposer import (
|
21
|
+
transpose,
|
22
|
+
Direction,
|
23
|
+
distance,
|
24
|
+
agnostic_distance,
|
25
|
+
transpose_agnostics,
|
26
|
+
transpose_encoding_to_agnostic,
|
27
|
+
transpose_agnostic_to_encoding,
|
28
|
+
)
|
29
|
+
|
30
|
+
from .pitch_models import (
|
31
|
+
AgnosticPitch,
|
32
|
+
pitches,
|
33
|
+
)
|
34
|
+
|
35
|
+
class Alteration(Enum):
|
36
|
+
"""
|
37
|
+
Enum for the alteration of a pitch.
|
38
|
+
"""
|
39
|
+
NONE = 0
|
40
|
+
SHARP = 1
|
41
|
+
FLAT = -1
|
42
|
+
DOUBLE_SHARP = 2
|
43
|
+
DOUBLE_FLAT = -2
|
44
|
+
TRIPLE_SHARP = 3
|
45
|
+
TRIPLE_FLAT = -3
|
46
|
+
HALF_SHARP = 0.5
|
47
|
+
HALF_FLAT = -0.5
|
48
|
+
QUARTER_SHARP = 0.25
|
49
|
+
QUARTER_FLAT = -0.25
|
50
|
+
|
51
|
+
def __str__(self) -> str:
|
52
|
+
return self.name
|
53
|
+
|
54
|
+
|
55
|
+
class PositionInStaff:
|
56
|
+
LINE_CHARACTER = 'L'
|
57
|
+
SPACE_CHARACTER = 'S'
|
58
|
+
|
59
|
+
def __init__(self, line_space: int):
|
60
|
+
"""
|
61
|
+
Initializes the PositionInStaff object.
|
62
|
+
|
63
|
+
Args:
|
64
|
+
line_space (int): 0 for bottom line, -1 for space under bottom line, 1 for space above bottom line. \
|
65
|
+
Increments by 1 for each line or space.
|
66
|
+
|
67
|
+
"""
|
68
|
+
self.line_space = line_space
|
69
|
+
|
70
|
+
@classmethod
|
71
|
+
def from_line(cls, line: int) -> PositionInStaff:
|
72
|
+
"""
|
73
|
+
Creates a PositionInStaff object from a line number.
|
74
|
+
|
75
|
+
Args:
|
76
|
+
line (int): The line number. line 1 is bottom line, 2 is the 1st line from bottom, 0 is the bottom ledger line
|
77
|
+
|
78
|
+
Returns:
|
79
|
+
PositionInStaff: The PositionInStaff object. 0 for the bottom line, 2 for the 1st line from bottom, -1 for the bottom ledger line, etc.
|
80
|
+
"""
|
81
|
+
return cls((line - 1) * 2)
|
82
|
+
|
83
|
+
@classmethod
|
84
|
+
def from_space(cls, space: int) -> PositionInStaff:
|
85
|
+
"""
|
86
|
+
Creates a PositionInStaff object from a space number.
|
87
|
+
|
88
|
+
Args:
|
89
|
+
space (int): The space number. space 1 is bottom space, 2
|
90
|
+
|
91
|
+
Returns:
|
92
|
+
PositionInStaff: The PositionInStaff object.
|
93
|
+
"""
|
94
|
+
return cls((space) * 2 - 1)
|
95
|
+
|
96
|
+
@classmethod
|
97
|
+
def from_encoded(cls, encoded: str) -> PositionInStaff:
|
98
|
+
"""
|
99
|
+
Creates a PositionInStaff object from an encoded string.
|
100
|
+
|
101
|
+
Args:
|
102
|
+
encoded (str): The encoded string.
|
103
|
+
|
104
|
+
Returns:
|
105
|
+
PositionInStaff: The PositionInStaff object.
|
106
|
+
"""
|
107
|
+
if encoded.startswith(cls.LINE_CHARACTER):
|
108
|
+
line = int(encoded[1:]) # Extract the line number
|
109
|
+
return cls.from_line(line)
|
110
|
+
elif encoded.startswith(cls.SPACE_CHARACTER):
|
111
|
+
space = int(encoded[1:]) # Extract the space number
|
112
|
+
return cls.from_space(space)
|
113
|
+
else:
|
114
|
+
raise ValueError(f""
|
115
|
+
f"Invalid encoded string: {encoded}. "
|
116
|
+
f"Expected to start with '{cls.LINE_CHARACTER}' or '{cls.SPACE_CHARACTER} at the beginning.")
|
117
|
+
|
118
|
+
|
119
|
+
def line(self):
|
120
|
+
"""
|
121
|
+
Returns the line number of the position in staff.
|
122
|
+
"""
|
123
|
+
return self.line_space // 2 + 1
|
124
|
+
|
125
|
+
|
126
|
+
def space(self):
|
127
|
+
"""
|
128
|
+
Returns the space number of the position in staff.
|
129
|
+
"""
|
130
|
+
return (self.line_space - 1) // 2 + 1
|
131
|
+
|
132
|
+
|
133
|
+
def is_line(self) -> bool:
|
134
|
+
"""
|
135
|
+
Returns True if the position is a line, False otherwise. If is not a line, it is a space, and vice versa.
|
136
|
+
"""
|
137
|
+
return self.line_space % 2 == 0
|
138
|
+
|
139
|
+
def move(self, line_space_difference: int) -> PositionInStaff:
|
140
|
+
"""
|
141
|
+
Returns a new PositionInStaff object with the position moved by the given number of lines or spaces.
|
142
|
+
|
143
|
+
Args:
|
144
|
+
line_space_difference (int): The number of lines or spaces to move.
|
145
|
+
|
146
|
+
Returns:
|
147
|
+
PositionInStaff: The new PositionInStaff object.
|
148
|
+
"""
|
149
|
+
return PositionInStaff(self.line_space + line_space_difference)
|
150
|
+
|
151
|
+
def position_below(self) -> PositionInStaff:
|
152
|
+
"""
|
153
|
+
Returns the position below the current position.
|
154
|
+
"""
|
155
|
+
return self.move(-2)
|
156
|
+
|
157
|
+
def position_above(self) -> PositionInStaff:
|
158
|
+
"""
|
159
|
+
Returns the position above the current position.
|
160
|
+
"""
|
161
|
+
return self.move(2)
|
162
|
+
|
163
|
+
|
164
|
+
|
165
|
+
def __str__(self) -> str:
|
166
|
+
"""
|
167
|
+
Returns the string representation of the position in staff.
|
168
|
+
"""
|
169
|
+
if self.is_line():
|
170
|
+
return f"{self.LINE_CHARACTER}{int(self.line())}"
|
171
|
+
else:
|
172
|
+
return f"{self.SPACE_CHARACTER}{int(self.space())}"
|
173
|
+
|
174
|
+
def __repr__(self) -> str:
|
175
|
+
"""
|
176
|
+
Returns the string representation of the PositionInStaff object.
|
177
|
+
"""
|
178
|
+
return f"PositionInStaff(line_space={self.line_space}), {self.__str__()}"
|
179
|
+
|
180
|
+
def __eq__(self, other) -> bool:
|
181
|
+
"""
|
182
|
+
Compares two PositionInStaff objects.
|
183
|
+
"""
|
184
|
+
if not isinstance(other, PositionInStaff):
|
185
|
+
return False
|
186
|
+
return self.line_space == other.line_space
|
187
|
+
|
188
|
+
def __ne__(self, other) -> bool:
|
189
|
+
"""
|
190
|
+
Compares two PositionInStaff objects.
|
191
|
+
"""
|
192
|
+
return not self.__eq__(other)
|
193
|
+
|
194
|
+
def __hash__(self) -> int:
|
195
|
+
"""
|
196
|
+
Returns the hash of the PositionInStaff object.
|
197
|
+
"""
|
198
|
+
return hash(self.line_space)
|
199
|
+
|
200
|
+
def __lt__(self, other) -> bool:
|
201
|
+
"""
|
202
|
+
Compares two PositionInStaff objects.
|
203
|
+
"""
|
204
|
+
if not isinstance(other, PositionInStaff):
|
205
|
+
return NotImplemented
|
206
|
+
return self.line_space < other.line_space
|
207
|
+
|
208
|
+
|
209
|
+
class DiatonicPitch:
|
210
|
+
def __init__(self, diatonic_pitch: str) -> None:
|
211
|
+
if diatonic_pitch not in pitches:
|
212
|
+
raise ValueError(f"Invalid diatonic pitch: {diatonic_pitch}. "
|
213
|
+
f"Expected one of {pitches}.")
|
214
|
+
self.encoding = diatonic_pitch
|
215
|
+
|
216
|
+
def __str__(self) -> str:
|
217
|
+
return self.encoding
|
218
|
+
|
219
|
+
|
220
|
+
class PitchPositionReferenceSystem:
|
221
|
+
def __init__(self, base_pitch: AgnosticPitch):
|
222
|
+
"""
|
223
|
+
Initializes the PitchPositionReferenceSystem object.
|
224
|
+
Args:
|
225
|
+
base_pitch (AgnosticPitch): The AgnosticPitch in the first line of the Staff. \
|
226
|
+
The AgnosticPitch object that serves as the reference point for the system.
|
227
|
+
"""
|
228
|
+
self.base_pitch = base_pitch
|
229
|
+
|
230
|
+
def compute_position(self, pitch: AgnosticPitch) -> PositionInStaff:
|
231
|
+
"""
|
232
|
+
Computes the position in staff for the given pitch.
|
233
|
+
Args:
|
234
|
+
pitch (AgnosticPitch): The AgnosticPitch object to compute the position for.
|
235
|
+
Returns:
|
236
|
+
PositionInStaff: The PositionInStaff object representing the computed position.
|
237
|
+
"""
|
238
|
+
# map A–G to 0–6
|
239
|
+
LETTER_TO_INDEX = {'C': 0, 'D': 1, 'E': 2,
|
240
|
+
'F': 3, 'G': 4, 'A': 5, 'B': 6}
|
241
|
+
|
242
|
+
# strip off any '+' or '-' accidentals, then grab the letter
|
243
|
+
def letter(p: AgnosticPitch) -> str:
|
244
|
+
name = p.name.replace('+', '').replace('-', '')
|
245
|
+
return AgnosticPitch(name, p.octave).name
|
246
|
+
|
247
|
+
base_letter_idx = LETTER_TO_INDEX[letter(self.base_pitch)]
|
248
|
+
target_letter_idx = LETTER_TO_INDEX[letter(pitch)]
|
249
|
+
|
250
|
+
# "octave difference × 7" plus the letter‐index difference
|
251
|
+
diatonic_steps = (pitch.octave - self.base_pitch.octave) * 7 \
|
252
|
+
+ (target_letter_idx - base_letter_idx)
|
253
|
+
|
254
|
+
# that many "lines or spaces" above (or below) the reference line
|
255
|
+
return PositionInStaff(diatonic_steps)
|
256
|
+
|
257
|
+
|
258
|
+
|
259
|
+
|
260
|
+
# TARGET: Retrieve agnostic encoding from semantic encoding
|
261
|
+
# INPUT: Pitch (Semantic) + Clef (Semantic)
|
262
|
+
# OUTPUT: PositionInStaff (Agnostic)
|
263
|
+
|
264
|
+
# CLEF has
|
265
|
+
# 1. bottom line equivalent to C4
|
266
|
+
# - C in 1st clef: 1st line is F2 == F
|
267
|
+
# - C in 2nd clef: 1st line is D2 == D
|
268
|
+
# - C in 3rd clef: 1st line is B2 == BB
|
269
|
+
# - C in 4th clef: 1st line is G2 == GG
|
270
|
+
# - G clef: 1st line is E4 == e
|
271
|
+
# - F in 3rd clef: 1st line is B2 == BB
|
272
|
+
# - F in 4th clef: 1st line is G2 == GG
|
273
|
+
|
274
|
+
# New abstraction:
|
275
|
+
# TARGET: PositionInStaff
|
276
|
+
# INPUT: Pitch (Semantic) + Reference System to the first line
|
277
|
+
# OUTPUT: PositionInStaff (Agnostic)
|
278
|
+
# Example:
|
279
|
+
# Input: Pitch (A4) + Reference System (Line 1 == E3) (Case of G Clef)
|
280
|
+
# >>> A4 is a 4th minor above E3 ¿How to calculate this?
|
281
|
+
# >>> a 4th minor is equivalent to the 2nd upper space
|
282
|
+
# >>> PositionInStaff.from_space(2)
|
283
|
+
# >>> PositionInStaff(?)
|
284
|
+
|
285
|
+
class Clef(ABC):
|
286
|
+
"""
|
287
|
+
Abstract class representing a clef.
|
288
|
+
"""
|
289
|
+
|
290
|
+
def __init__(self, diatonic_pitch: DiatonicPitch, on_line: int):
|
291
|
+
"""
|
292
|
+
Initializes the Clef object.
|
293
|
+
Args:
|
294
|
+
diatonic_pitch (DiatonicPitch): The diatonic pitch of the clef (e.g., 'C', 'G', 'F'). This value is used as a decorator.
|
295
|
+
on_line (int): The line number on which the clef is placed (1 for bottom line, 2 for 1st line from bottom, etc.). This value is used as a decorator.
|
296
|
+
"""
|
297
|
+
self.diatonic_pitch = diatonic_pitch
|
298
|
+
self.on_line = on_line
|
299
|
+
|
300
|
+
@abstractmethod
|
301
|
+
def bottom_line(self) -> AgnosticPitch:
|
302
|
+
"""
|
303
|
+
Returns the pitch of the bottom line of the staff.
|
304
|
+
"""
|
305
|
+
...
|
306
|
+
|
307
|
+
def name(self):
|
308
|
+
"""
|
309
|
+
Returns the name of the clef.
|
310
|
+
"""
|
311
|
+
return f"{self.diatonic_pitch} on line {self.on_line}"
|
312
|
+
|
313
|
+
def reference_point(self) -> PitchPositionReferenceSystem:
|
314
|
+
"""
|
315
|
+
Returns the reference point for the clef.
|
316
|
+
"""
|
317
|
+
return PitchPositionReferenceSystem(self.bottom_line())
|
318
|
+
|
319
|
+
def __str__(self) -> str:
|
320
|
+
"""
|
321
|
+
Returns:
|
322
|
+
str: The string representation of the clef.
|
323
|
+
"""
|
324
|
+
return f'{self.diatonic_pitch.encoding.upper()} on the {self.on_line}{self._ordinal_suffix(self.on_line)} line'
|
325
|
+
|
326
|
+
@staticmethod
|
327
|
+
def _ordinal_suffix(number: int) -> str:
|
328
|
+
"""
|
329
|
+
Returns the ordinal suffix for a given integer (e.g. 'st', 'nd', 'rd', 'th').
|
330
|
+
|
331
|
+
Args:
|
332
|
+
number (int): The number to get the suffix for.
|
333
|
+
|
334
|
+
Returns:
|
335
|
+
str: The ordinal suffix.
|
336
|
+
"""
|
337
|
+
# 11, 12, 13 always take “th”
|
338
|
+
if 11 <= (number % 100) <= 13:
|
339
|
+
return 'th'
|
340
|
+
# otherwise use last digit
|
341
|
+
last = number % 10
|
342
|
+
if last == 1:
|
343
|
+
return 'st'
|
344
|
+
elif last == 2:
|
345
|
+
return 'nd'
|
346
|
+
elif last == 3:
|
347
|
+
return 'rd'
|
348
|
+
else:
|
349
|
+
return 'th'
|
350
|
+
|
351
|
+
|
352
|
+
class GClef(Clef):
|
353
|
+
def __init__(self):
|
354
|
+
"""
|
355
|
+
Initializes the G Clef object.
|
356
|
+
"""
|
357
|
+
super().__init__(DiatonicPitch('G'), 2)
|
358
|
+
|
359
|
+
def bottom_line(self) -> AgnosticPitch:
|
360
|
+
"""
|
361
|
+
Returns the pitch of the bottom line of the staff.
|
362
|
+
"""
|
363
|
+
return AgnosticPitch('E', 4)
|
364
|
+
|
365
|
+
class F3Clef(Clef):
|
366
|
+
def __init__(self):
|
367
|
+
"""
|
368
|
+
Initializes the F Clef object.
|
369
|
+
"""
|
370
|
+
super().__init__(DiatonicPitch('F'), 3)
|
371
|
+
|
372
|
+
def bottom_line(self) -> AgnosticPitch:
|
373
|
+
"""
|
374
|
+
Returns the pitch of the bottom line of the staff.
|
375
|
+
"""
|
376
|
+
return AgnosticPitch('B', 3)
|
377
|
+
|
378
|
+
class F4Clef(Clef):
|
379
|
+
def __init__(self):
|
380
|
+
"""
|
381
|
+
Initializes the F Clef object.
|
382
|
+
"""
|
383
|
+
super().__init__(DiatonicPitch('F'), 4)
|
384
|
+
|
385
|
+
def bottom_line(self) -> AgnosticPitch:
|
386
|
+
"""
|
387
|
+
Returns the pitch of the bottom line of the staff.
|
388
|
+
"""
|
389
|
+
return AgnosticPitch('G', 2)
|
390
|
+
|
391
|
+
class C1Clef(Clef):
|
392
|
+
def __init__(self):
|
393
|
+
"""
|
394
|
+
Initializes the C Clef object.
|
395
|
+
"""
|
396
|
+
super().__init__(DiatonicPitch('C'), 1)
|
397
|
+
|
398
|
+
def bottom_line(self) -> AgnosticPitch:
|
399
|
+
"""
|
400
|
+
Returns the pitch of the bottom line of the staff.
|
401
|
+
"""
|
402
|
+
return AgnosticPitch('C', 3)
|
403
|
+
|
404
|
+
class C2Clef(Clef):
|
405
|
+
def __init__(self):
|
406
|
+
"""
|
407
|
+
Initializes the C Clef object.
|
408
|
+
"""
|
409
|
+
super().__init__(DiatonicPitch('A'), 2)
|
410
|
+
|
411
|
+
def bottom_line(self) -> AgnosticPitch:
|
412
|
+
"""
|
413
|
+
Returns the pitch of the bottom line of the staff.
|
414
|
+
"""
|
415
|
+
return AgnosticPitch('A', 2)
|
416
|
+
|
417
|
+
|
418
|
+
class C3Clef(Clef):
|
419
|
+
def __init__(self):
|
420
|
+
"""
|
421
|
+
Initializes the C Clef object.
|
422
|
+
"""
|
423
|
+
super().__init__(DiatonicPitch('C'), 3)
|
424
|
+
|
425
|
+
def bottom_line(self) -> AgnosticPitch:
|
426
|
+
"""
|
427
|
+
Returns the pitch of the bottom line of the staff.
|
428
|
+
"""
|
429
|
+
return AgnosticPitch('B', 2)
|
430
|
+
|
431
|
+
class C4Clef(Clef):
|
432
|
+
def __init__(self):
|
433
|
+
"""
|
434
|
+
Initializes the C Clef object.
|
435
|
+
"""
|
436
|
+
super().__init__(DiatonicPitch('C'), 4)
|
437
|
+
|
438
|
+
def bottom_line(self) -> AgnosticPitch:
|
439
|
+
"""
|
440
|
+
Returns the pitch of the bottom line of the staff.
|
441
|
+
"""
|
442
|
+
return AgnosticPitch('D', 2)
|
443
|
+
|
444
|
+
|
445
|
+
class ClefFactory:
|
446
|
+
CLEF_NAMES = { 'G', 'F', 'C' }
|
447
|
+
@classmethod
|
448
|
+
def create_clef(cls, encoding: str) -> Clef:
|
449
|
+
"""
|
450
|
+
Creates a Clef object based on the given token.
|
451
|
+
|
452
|
+
Clefs are encoded in interpretation tokens that start with a single * followed by the string clef and then the shape and line position of the clef. For example, a treble clef is *clefG2, with G meaning a G-clef, and 2 meaning that the clef is centered on the second line up from the bottom of the staff. The bass clef is *clefF4 since it is an F-clef on the fourth line of the staff.
|
453
|
+
A vocal tenor clef is represented by *clefGv2, where the v means the music should be played an octave lower than the regular clef’s sounding pitches. Try creating a vocal tenor clef in the above interactive example. The v operator also works on the other clefs (but these sorts of clefs are very rare). Another rare clef is *clefG^2 which is the opposite of *clefGv2, where the music is written an octave lower than actually sounding pitch for the normal form of the clef. You can also try to create exotic two-octave clefs by doubling the ^^ and vv markers.
|
454
|
+
|
455
|
+
Args:
|
456
|
+
encoding (str): The encoding of the clef token.
|
457
|
+
|
458
|
+
Returns:
|
459
|
+
|
460
|
+
"""
|
461
|
+
encoding = encoding.replace('*clef', '')
|
462
|
+
|
463
|
+
# at this point the encoding is like G2, F4,... or Gv2, F^4,... or G^^2, Fvv4,... or G^^...^^2, Fvvv4,...
|
464
|
+
name = list(filter(lambda x: x in cls.CLEF_NAMES, encoding))[0]
|
465
|
+
line = int(list(filter(lambda x: x.isdigit(), encoding))[0])
|
466
|
+
decorators = ''.join(filter(lambda x: x in ['^', 'v'], encoding))
|
467
|
+
|
468
|
+
if name not in cls.CLEF_NAMES:
|
469
|
+
raise ValueError(f"Invalid clef name: {name}. Expected one of {cls.CLEF_NAMES}.")
|
470
|
+
|
471
|
+
if name == 'G':
|
472
|
+
return GClef()
|
473
|
+
elif name == 'F':
|
474
|
+
if line == 3:
|
475
|
+
return F3Clef()
|
476
|
+
elif line == 4:
|
477
|
+
return F4Clef()
|
478
|
+
else:
|
479
|
+
raise ValueError(f"Invalid F clef line: {line}. Expected 3 or 4.")
|
480
|
+
elif name == 'C':
|
481
|
+
if line == 1:
|
482
|
+
return C1Clef()
|
483
|
+
elif line == 2:
|
484
|
+
return C2Clef()
|
485
|
+
elif line == 3:
|
486
|
+
return C3Clef()
|
487
|
+
elif line == 4:
|
488
|
+
return C4Clef()
|
489
|
+
else:
|
490
|
+
raise ValueError(f"Invalid C clef line: {line}. Expected 1, 2, 3 or 4.")
|
491
|
+
else:
|
492
|
+
raise ValueError(f"Invalid clef name: {name}. Expected one of {cls.CLEF_NAMES}.")
|
493
|
+
|
494
|
+
|
495
|
+
|
496
|
+
|
497
|
+
|
498
|
+
|
499
|
+
class Staff:
|
500
|
+
def position_in_staff(self, *, clef: Clef, pitch: AgnosticPitch) -> PositionInStaff:
|
501
|
+
"""
|
502
|
+
Returns the position in staff for the given clef and pitch.
|
503
|
+
"""
|
504
|
+
bottom_cleff_note_name = clef.bottom_line()
|
505
|
+
|
506
|
+
|
507
|
+
|
508
|
+
class GKernExporter:
|
509
|
+
def __init__(self, clef: Clef):
|
510
|
+
self.clef = clef
|
511
|
+
|
512
|
+
def export(self, staff: Staff, pitch: AgnosticPitch) -> str:
|
513
|
+
"""
|
514
|
+
Exports the given pitch to a graphic **kern encoding.
|
515
|
+
"""
|
516
|
+
position = self.agnostic_position(staff, pitch)
|
517
|
+
return f"{GRAPHIC_TOKEN_SEPARATOR}{str(position)}"
|
518
|
+
|
519
|
+
def agnostic_position(self, staff: Staff, pitch: AgnosticPitch) -> PositionInStaff:
|
520
|
+
"""
|
521
|
+
Returns the agnostic position in staff for the given pitch.
|
522
|
+
"""
|
523
|
+
return staff.position_in_staff(clef=self.clef, pitch=pitch)
|
524
|
+
|
525
|
+
|
526
|
+
|
@@ -0,0 +1,89 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import sys
|
4
|
+
from pathlib import Path
|
5
|
+
from unicodedata import category
|
6
|
+
|
7
|
+
from kernpy.core import Token, SpineOperationToken
|
8
|
+
from kernpy.core.document import MultistageTree, Node
|
9
|
+
|
10
|
+
|
11
|
+
class GraphvizExporter:
|
12
|
+
def export_token(self, token: Token):
|
13
|
+
if token is None or token.encoding is None:
|
14
|
+
return ''
|
15
|
+
else:
|
16
|
+
return token.encoding.replace('\"', '\\"').replace('\\', '\\\\')
|
17
|
+
|
18
|
+
@staticmethod
|
19
|
+
def node_id(node: Node):
|
20
|
+
return f"node{id(node)}"
|
21
|
+
|
22
|
+
def export_to_dot(self, tree: MultistageTree, filename: Path = None):
|
23
|
+
"""
|
24
|
+
Export the given MultistageTree to DOT format.
|
25
|
+
|
26
|
+
Args:
|
27
|
+
tree (MultistageTree): The tree to export.
|
28
|
+
filename (Path or None): The output file path. If None, prints to stdout.
|
29
|
+
"""
|
30
|
+
file = sys.stdout if filename is None else open(filename, 'w')
|
31
|
+
|
32
|
+
try:
|
33
|
+
file.write('digraph G {\n')
|
34
|
+
file.write(' node [shape=record];\n')
|
35
|
+
file.write(' rankdir=TB;\n') # Ensure top-to-bottom layout
|
36
|
+
|
37
|
+
# Create subgraphs for each stage
|
38
|
+
for stage_index, stage in enumerate(tree.stages):
|
39
|
+
if stage:
|
40
|
+
file.write(' {rank=same; ')
|
41
|
+
for node in stage:
|
42
|
+
file.write(f'"{self.node_id(node)}"; ')
|
43
|
+
file.write('}\n')
|
44
|
+
|
45
|
+
# Write nodes and their connections
|
46
|
+
self._write_nodes_iterative(tree.root, file)
|
47
|
+
self._write_edges_iterative(tree.root, file)
|
48
|
+
|
49
|
+
file.write('}\n')
|
50
|
+
|
51
|
+
finally:
|
52
|
+
if filename is not None:
|
53
|
+
file.close() # Close only if we explicitly opened a file
|
54
|
+
|
55
|
+
def _write_nodes_iterative(self, root, file):
|
56
|
+
stack = [root]
|
57
|
+
|
58
|
+
while stack:
|
59
|
+
node = stack.pop()
|
60
|
+
header_label = f'header #{node.header_node.id}' if node.header_node else ''
|
61
|
+
last_spine_operator_label = f'last spine op. #{node.last_spine_operator_node.id}' if node.last_spine_operator_node else ''
|
62
|
+
category_name = getattr(getattr(getattr(node, "token", None), "category", None), "_name_", "Non defined category")
|
63
|
+
|
64
|
+
|
65
|
+
top_record_label = f'{{ #{node.id}| stage {node.stage} | {header_label} | {last_spine_operator_label} | {category_name} }}'
|
66
|
+
signatures_label = ''
|
67
|
+
if node.last_signature_nodes and node.last_signature_nodes.nodes:
|
68
|
+
for k, v in node.last_signature_nodes.nodes.items():
|
69
|
+
if signatures_label:
|
70
|
+
signatures_label += '|'
|
71
|
+
signatures_label += f'{k} #{v.id}'
|
72
|
+
|
73
|
+
if isinstance(node.token, SpineOperationToken) and node.token.cancelled_at_stage:
|
74
|
+
signatures_label += f'| {{ cancelled at stage {node.token.cancelled_at_stage} }}'
|
75
|
+
|
76
|
+
file.write(f' "{self.node_id(node)}" [label="{{ {top_record_label} | {signatures_label} | {self.export_token(node.token)} }}"];\n')
|
77
|
+
|
78
|
+
# Add children to the stack to be processed
|
79
|
+
for child in reversed(node.children):
|
80
|
+
stack.append(child)
|
81
|
+
|
82
|
+
def _write_edges_iterative(self, root, file):
|
83
|
+
stack = [root]
|
84
|
+
|
85
|
+
while stack:
|
86
|
+
node = stack.pop()
|
87
|
+
for child in node.children:
|
88
|
+
file.write(f' "{self.node_id(node)}" -> "{self.node_id(child)}";\n')
|
89
|
+
stack.append(child)
|
@@ -0,0 +1,41 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
from typing import Optional
|
3
|
+
|
4
|
+
from .spine_importer import SpineImporter
|
5
|
+
from .tokens import SimpleToken, TokenCategory, Token
|
6
|
+
from .kern_spine_importer import KernSpineListener, KernSpineImporter
|
7
|
+
from .base_antlr_spine_parser_listener import BaseANTLRSpineParserListener
|
8
|
+
|
9
|
+
|
10
|
+
class HarmSpineImporter(SpineImporter):
|
11
|
+
def __init__(self, verbose: Optional[bool] = False):
|
12
|
+
"""
|
13
|
+
KernSpineImporter constructor.
|
14
|
+
|
15
|
+
Args:
|
16
|
+
verbose (Optional[bool]): Level of verbosity for error messages.
|
17
|
+
"""
|
18
|
+
super().__init__(verbose=verbose)
|
19
|
+
|
20
|
+
def import_listener(self) -> BaseANTLRSpineParserListener:
|
21
|
+
return KernSpineListener()
|
22
|
+
|
23
|
+
def import_token(self, encoding: str) -> Token:
|
24
|
+
self._raise_error_if_wrong_input(encoding)
|
25
|
+
|
26
|
+
kern_spine_importer = KernSpineImporter()
|
27
|
+
token = kern_spine_importer.import_token(encoding)
|
28
|
+
|
29
|
+
ACCEPTED_CATEGORIES = {
|
30
|
+
TokenCategory.STRUCTURAL,
|
31
|
+
TokenCategory.SIGNATURES,
|
32
|
+
TokenCategory.EMPTY,
|
33
|
+
TokenCategory.IMAGE_ANNOTATIONS,
|
34
|
+
TokenCategory.BARLINES,
|
35
|
+
TokenCategory.COMMENTS,
|
36
|
+
}
|
37
|
+
|
38
|
+
if not any(TokenCategory.is_child(child=token.category, parent=cat) for cat in ACCEPTED_CATEGORIES):
|
39
|
+
return SimpleToken(encoding, TokenCategory.HARMONY)
|
40
|
+
|
41
|
+
return token
|