abcnotes 0.1.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.
abcnotes.py
ADDED
|
@@ -0,0 +1,1527 @@
|
|
|
1
|
+
from __future__ import division
|
|
2
|
+
import re, sys
|
|
3
|
+
from fractions import Fraction
|
|
4
|
+
from collections import namedtuple
|
|
5
|
+
|
|
6
|
+
__version__ = '0.1.0'
|
|
7
|
+
|
|
8
|
+
str_type = str if sys.version > '3' else basestring
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# Just some tunes to test against
|
|
12
|
+
tunes = [
|
|
13
|
+
"""
|
|
14
|
+
X: 1
|
|
15
|
+
T: The Road To Lisdoonvarna
|
|
16
|
+
R: slide
|
|
17
|
+
M: 12/8
|
|
18
|
+
L: 1/8
|
|
19
|
+
K: Edor
|
|
20
|
+
E2B B2A B2c d2A|F2A ABA D2E FED|E2B B2A B2c d3|cdc B2A B2E E3:|
|
|
21
|
+
|:e2f gfe d2B Bcd|c2A ABc d2B B3|e2f gfe d2B Bcd|cdc B2A B2E E3:||
|
|
22
|
+
""",
|
|
23
|
+
"""
|
|
24
|
+
X: 6
|
|
25
|
+
T: The Kid On The Mountain
|
|
26
|
+
R: slip jig
|
|
27
|
+
M: 9/8
|
|
28
|
+
L: 1/8
|
|
29
|
+
K: Emin
|
|
30
|
+
~E3 FEF G2 F| ~E3 BcA BGD| ~E3 FEF G2 A| BAG FAG FED:|
|
|
31
|
+
BGB AFD G2 D| GAB dge dBA| BGB AFA G2 A| BAG FAG FED:|
|
|
32
|
+
~g3 eBe e2 f|~g3 efg afd| ~g3 eBe g2 a|bag fag fed:|
|
|
33
|
+
eB/B/B e2f ~g3|eB/B/B efg afd| eB/B/B e2f g2a|bag fag fed:|
|
|
34
|
+
edB dBA G2D|GAB dge dBA|edB dBA G2A|BAG FAG FED:|
|
|
35
|
+
"""
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
# Information field table copied from
|
|
39
|
+
# http://abcnotation.com/wiki/abc:standard:v2.1#abc_files_tunes_and_fragments
|
|
40
|
+
# Columns are:
|
|
41
|
+
# X:Field, file header, tune header, tune body, inline, type
|
|
42
|
+
information_field_table = """
|
|
43
|
+
A:area yes yes no no string
|
|
44
|
+
B:book yes yes no no string
|
|
45
|
+
C:composer yes yes no no string
|
|
46
|
+
D:discography yes yes no no string
|
|
47
|
+
F:file url yes yes no no string
|
|
48
|
+
G:group yes yes no no string
|
|
49
|
+
H:history yes yes no no string
|
|
50
|
+
I:instruction yes yes yes yes instruction
|
|
51
|
+
K:key no yes yes yes instruction
|
|
52
|
+
L:unit note length yes yes yes yes instruction
|
|
53
|
+
M:meter yes yes yes yes instruction
|
|
54
|
+
m:macro yes yes yes yes instruction
|
|
55
|
+
N:notes yes yes yes yes string
|
|
56
|
+
O:origin yes yes no no string
|
|
57
|
+
P:parts no yes yes yes instruction
|
|
58
|
+
Q:tempo no yes yes yes instruction
|
|
59
|
+
R:rhythm yes yes yes yes string
|
|
60
|
+
r:remark yes yes yes yes -
|
|
61
|
+
S:source yes yes no no string
|
|
62
|
+
s:symbol line no no yes no instruction
|
|
63
|
+
T:tune title no yes yes no string
|
|
64
|
+
U:user defined yes yes yes yes instruction
|
|
65
|
+
V:voice no yes yes yes instruction
|
|
66
|
+
W:words no yes yes no string
|
|
67
|
+
w:words no no yes no string
|
|
68
|
+
X:reference number no yes no no instruction
|
|
69
|
+
Z:transcription yes yes no no string
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
class InfoKey(object):
|
|
73
|
+
def __init__(self, key, name, file_header, tune_header, tune_body, inline, type):
|
|
74
|
+
self.key = key # single-letter field identifier
|
|
75
|
+
self.name = name.strip() # information field name
|
|
76
|
+
self.file_header = file_header=='yes' # may be used in file header
|
|
77
|
+
self.tune_header = tune_header=='yes' # may be used in tune header
|
|
78
|
+
self.tune_body = tune_body=='yes' # may be used in tune body
|
|
79
|
+
self.inline = inline=='yes' # nay be used inline in tunes
|
|
80
|
+
self.type = type.strip() # data type: string, instruction, or -
|
|
81
|
+
|
|
82
|
+
# parse info field table
|
|
83
|
+
info_keys = {}
|
|
84
|
+
for line in information_field_table.split('\n'):
|
|
85
|
+
if line.strip() == '':
|
|
86
|
+
continue
|
|
87
|
+
key = line[0]
|
|
88
|
+
fields = re.match(r'(.*)\s+(yes|no)\s+(yes|no)\s+(yes|no)\s+(yes|no)\s+(.*)', line[2:]).groups()
|
|
89
|
+
info_keys[key] = InfoKey(key, *fields)
|
|
90
|
+
|
|
91
|
+
file_header_fields = {k:v for k,v in info_keys.items() if v.file_header}
|
|
92
|
+
tune_header_fields = {k:v for k,v in info_keys.items() if v.tune_header}
|
|
93
|
+
tune_body_fields = {k:v for k,v in info_keys.items() if v.tune_body}
|
|
94
|
+
inline_fields = {k:v for k,v in info_keys.items() if v.inline}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# map natural note letters to chromatic values
|
|
99
|
+
pitch_values = {'C': 0, 'D': 2, 'E': 4, 'F': 5, 'G': 7, 'A': 9, 'B': 11, }
|
|
100
|
+
accidental_values = {'': 0, '#': 1, 'b': -1, '##': 2, 'bb': -2}
|
|
101
|
+
# accidental written in a K: field (K:D ^g) -> accidental name used above
|
|
102
|
+
key_accidental_names = {'=': '', '^': '#', '^^': '##', '_': 'b', '__': 'bb'}
|
|
103
|
+
# semitone offset of an accidental written on a note inside the tune body
|
|
104
|
+
note_accidental_values = {'=': 0, '^': 1, '^^': 2, '_': -1, '__': -2}
|
|
105
|
+
for n,v in list(pitch_values.items()):
|
|
106
|
+
for a in '#b':
|
|
107
|
+
pitch_values[n+a] = v + accidental_values[a]
|
|
108
|
+
|
|
109
|
+
# map chromatic number back to most common key names
|
|
110
|
+
chromatic_notes = ['C', 'C#', 'D', 'Eb', 'E', 'F', 'F#', 'G', 'Ab', 'A', 'Bb', 'B']
|
|
111
|
+
|
|
112
|
+
# map mode names relative to Ionian (in chromatic steps)
|
|
113
|
+
mode_values = {'major': 0, 'minor': 3, 'ionian': 0, 'aeolian': 3,
|
|
114
|
+
'mixolydian': -7, 'dorian': -2, 'phrygian': -4, 'lydian': -5,
|
|
115
|
+
'locrian': 1}
|
|
116
|
+
|
|
117
|
+
# mode name normalization
|
|
118
|
+
mode_abbrev = {m[:3]: m for m in mode_values}
|
|
119
|
+
|
|
120
|
+
# sharps/flats in ionian keys
|
|
121
|
+
key_sig = {'C#': 7, 'F#': 6, 'B': 5, 'E': 4, 'A': 3, 'D': 2, 'G': 1, 'C': 0,
|
|
122
|
+
'F': -1, 'Bb': -2, 'Eb': -3, 'Ab': -4, 'Db': -5, 'Gb': -6, 'Cb': -7}
|
|
123
|
+
sharp_order = "FCGDAEB"
|
|
124
|
+
flat_order = "BEADGCF"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class Key(object):
|
|
128
|
+
def __init__(self, name=None, root=None, mode=None, base=None):
|
|
129
|
+
# accidentals written after the tonic (K:D ^g): letter -> '#', 'b', ''
|
|
130
|
+
self.extra_accidentals = {}
|
|
131
|
+
# K:D exp ^f ^c means only the explicit accidentals, no key signature
|
|
132
|
+
self.explicit_only = False
|
|
133
|
+
if name is not None:
|
|
134
|
+
self.root, self.mode = self.parse_key(name, base)
|
|
135
|
+
assert root is None and mode is None
|
|
136
|
+
else:
|
|
137
|
+
self.root = Pitch(root)
|
|
138
|
+
self.mode = mode
|
|
139
|
+
|
|
140
|
+
def parse_key(self, key, base=None):
|
|
141
|
+
key = key.strip()
|
|
142
|
+
self.explicit_only = re.search(r'\bexp\b', key) is not None
|
|
143
|
+
# drop clef and other specifiers: clef=bass middle=d transpose=-2
|
|
144
|
+
# bare clef names, and the "exp" (explicit accidentals) keyword
|
|
145
|
+
key = re.sub(r'\s*\b(\w+=\S*|treble|alto|tenor|bass|perc|exp)\b', '', key).strip()
|
|
146
|
+
# explicit accidentals after the tonic: K:D ^g K:F =b K:C exp ^f
|
|
147
|
+
for acc, letter in re.findall(r'(\^\^|\^|__|_|=)([a-gA-G])', key):
|
|
148
|
+
self.extra_accidentals[letter.upper()] = key_accidental_names[acc]
|
|
149
|
+
key = re.sub(r'\s*(\^\^|\^|__|_|=)[a-gA-G]', '', key).strip()
|
|
150
|
+
# accidentals only, e.g. an inline [K:=f]: modify the current key
|
|
151
|
+
if key == '' and base is not None:
|
|
152
|
+
accidentals = dict(base.extra_accidentals)
|
|
153
|
+
accidentals.update(self.extra_accidentals)
|
|
154
|
+
self.extra_accidentals = accidentals
|
|
155
|
+
self.explicit_only = base.explicit_only
|
|
156
|
+
return base.root, base.mode
|
|
157
|
+
# no key signature (K:none, or an empty K: field)
|
|
158
|
+
if key in ['', 'none']:
|
|
159
|
+
return Pitch('C'), 'major'
|
|
160
|
+
# highland pipe keys: F# and C# (with G natural), i.e. the D signature.
|
|
161
|
+
# HP displays no signature and Hp displays it; the pitches are the same.
|
|
162
|
+
if key in ['HP', 'Hp']:
|
|
163
|
+
return Pitch('D'), 'major'
|
|
164
|
+
|
|
165
|
+
m = re.match(r'([A-G])(\#|b)?\s*(\w+)?(.*)', key)
|
|
166
|
+
if m is None:
|
|
167
|
+
# retry allowing a lower-case tonic (e.g. "emin")
|
|
168
|
+
key = key[:1].upper() + key[1:]
|
|
169
|
+
m = re.match(r'([A-G])(\#|b)?\s*(\w+)?(.*)', key)
|
|
170
|
+
if m is None:
|
|
171
|
+
raise ValueError('Invalid key "%s"' % key)
|
|
172
|
+
base, acc, mode, extra = m.groups()
|
|
173
|
+
if acc is None:
|
|
174
|
+
acc = ''
|
|
175
|
+
if mode is None:
|
|
176
|
+
mode = 'major'
|
|
177
|
+
if mode == 'm':
|
|
178
|
+
mode = 'minor'
|
|
179
|
+
try:
|
|
180
|
+
mode = mode_abbrev[mode[:3].lower()]
|
|
181
|
+
except KeyError:
|
|
182
|
+
raise ValueError("Unrecognized key signature %s" % key)
|
|
183
|
+
|
|
184
|
+
return Pitch(base+acc), mode
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
def key_signature(self):
|
|
188
|
+
"""
|
|
189
|
+
List of accidentals that should be displayed in the key
|
|
190
|
+
signature for the given key description.
|
|
191
|
+
"""
|
|
192
|
+
# determine number of sharps/flats for this key by first converting
|
|
193
|
+
# to ionian, then doing the key lookup
|
|
194
|
+
key = self.relative_ionian
|
|
195
|
+
num_acc = key_sig[key.root.name]
|
|
196
|
+
|
|
197
|
+
sig = []
|
|
198
|
+
# sharps or flats?
|
|
199
|
+
if num_acc > 0:
|
|
200
|
+
for i in range(num_acc):
|
|
201
|
+
sig.append(sharp_order[i] + '#')
|
|
202
|
+
else:
|
|
203
|
+
for i in range(-num_acc):
|
|
204
|
+
sig.append(flat_order[i] + 'b')
|
|
205
|
+
|
|
206
|
+
return sig
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def accidentals(self):
|
|
210
|
+
"""A dictionary of accidentals in the key signature, including any
|
|
211
|
+
written explicitly in the K: field.
|
|
212
|
+
"""
|
|
213
|
+
acc = {} if self.explicit_only else {s[0]: s[1:] for s in self.key_signature}
|
|
214
|
+
acc.update(self.extra_accidentals)
|
|
215
|
+
return acc
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def relative_ionian(self):
|
|
219
|
+
"""
|
|
220
|
+
Return the ionian mode relative to the given key and mode.
|
|
221
|
+
"""
|
|
222
|
+
key, mode = self.root, self.mode
|
|
223
|
+
rel = mode_values[mode]
|
|
224
|
+
root = Pitch((key.value + rel) % 12)
|
|
225
|
+
|
|
226
|
+
# Select flat or sharp to match the current key name
|
|
227
|
+
if '#' in key.name:
|
|
228
|
+
root2 = root.equivalent_sharp
|
|
229
|
+
if len(root2.name) == 2:
|
|
230
|
+
root = root2
|
|
231
|
+
elif 'b' in key.name:
|
|
232
|
+
root2 = root.equivalent_flat
|
|
233
|
+
if len(root2.name) == 2:
|
|
234
|
+
root = root2
|
|
235
|
+
|
|
236
|
+
return Key(root=root, mode='ionian')
|
|
237
|
+
|
|
238
|
+
def __repr__(self):
|
|
239
|
+
return "<Key %s %s>" % (self.root.name, self.mode)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class Pitch(object):
|
|
243
|
+
def __init__(self, value, octave=None):
|
|
244
|
+
if isinstance(value, Note):
|
|
245
|
+
self._note = value
|
|
246
|
+
|
|
247
|
+
# A note's chromatic value is its natural pitch plus one accidental.
|
|
248
|
+
# An explicit accidental on the note (^ ^^ = _ __) takes priority;
|
|
249
|
+
# otherwise the key signature supplies the accidental for the letter.
|
|
250
|
+
letter = value.note.upper()
|
|
251
|
+
accidental = value.accidental
|
|
252
|
+
if accidental is None:
|
|
253
|
+
accidental = value.bar_accidental # carried from earlier in the bar
|
|
254
|
+
if accidental is None:
|
|
255
|
+
acc = value.key.accidentals.get(letter, '') # '#', 'b' or ''
|
|
256
|
+
self._name = letter + acc
|
|
257
|
+
self._value = pitch_values[letter] + accidental_values[acc]
|
|
258
|
+
else:
|
|
259
|
+
self._name = value.note.capitalize()
|
|
260
|
+
self._value = pitch_values[letter] + note_accidental_values[accidental]
|
|
261
|
+
|
|
262
|
+
assert octave is None
|
|
263
|
+
self._octave = value.octave
|
|
264
|
+
elif isinstance(value, str_type):
|
|
265
|
+
self._name = value
|
|
266
|
+
self._value = self.pitch_value(value)
|
|
267
|
+
self._octave = octave
|
|
268
|
+
elif isinstance(value, Pitch):
|
|
269
|
+
self._name = value._name
|
|
270
|
+
self._value = value._value
|
|
271
|
+
self._octave = value._octave
|
|
272
|
+
else:
|
|
273
|
+
self._name = None
|
|
274
|
+
if octave is None:
|
|
275
|
+
self._value = value
|
|
276
|
+
self._octave = octave
|
|
277
|
+
else:
|
|
278
|
+
self._value = value % 12
|
|
279
|
+
self._octave = octave + (value // 12)
|
|
280
|
+
|
|
281
|
+
def __repr__(self):
|
|
282
|
+
return "<Pitch %s>" % self.name
|
|
283
|
+
|
|
284
|
+
@property
|
|
285
|
+
def name(self):
|
|
286
|
+
if self._name is not None:
|
|
287
|
+
return self._name
|
|
288
|
+
return chromatic_notes[self.value%12]
|
|
289
|
+
|
|
290
|
+
@property
|
|
291
|
+
def value(self):
|
|
292
|
+
return self._value
|
|
293
|
+
|
|
294
|
+
@property
|
|
295
|
+
def octave(self):
|
|
296
|
+
return self._octave
|
|
297
|
+
|
|
298
|
+
@property
|
|
299
|
+
def abs_value(self):
|
|
300
|
+
return self.value + self.octave * 12
|
|
301
|
+
|
|
302
|
+
@staticmethod
|
|
303
|
+
def pitch_value(pitch, root='C'):
|
|
304
|
+
"""Convert a pitch string like "A#" to a chromatic scale value relative
|
|
305
|
+
to root.
|
|
306
|
+
"""
|
|
307
|
+
pitch = pitch.strip()
|
|
308
|
+
val = pitch_values[pitch[0].upper()]
|
|
309
|
+
for acc in pitch[1:]:
|
|
310
|
+
val += accidental_values[acc]
|
|
311
|
+
if root == 'C':
|
|
312
|
+
return val
|
|
313
|
+
return (val - Pitch.pitch_value(root)) % 12
|
|
314
|
+
|
|
315
|
+
def __eq__(self, a):
|
|
316
|
+
return self.value == a.value
|
|
317
|
+
|
|
318
|
+
@property
|
|
319
|
+
def equivalent_sharp(self):
|
|
320
|
+
p = self - 1
|
|
321
|
+
if len(p.name) == 1:
|
|
322
|
+
return Pitch(p.name + '#', octave=self.octave)
|
|
323
|
+
else:
|
|
324
|
+
return Pitch((self-2).name + '##', octave=self.octave)
|
|
325
|
+
|
|
326
|
+
@property
|
|
327
|
+
def equivalent_flat(self):
|
|
328
|
+
p = self + 1
|
|
329
|
+
if len(p.name) == 1:
|
|
330
|
+
return Pitch(p.name + 'b', octave=self.octave)
|
|
331
|
+
else:
|
|
332
|
+
return Pitch((self+2).name + 'bb', octave=self.octave)
|
|
333
|
+
|
|
334
|
+
def __add__(self, x):
|
|
335
|
+
return Pitch(self.value+x, octave=self.octave)
|
|
336
|
+
|
|
337
|
+
def __sub__(self, x):
|
|
338
|
+
return Pitch(self.value-x, octave=self.octave)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def parse_meter(meter):
|
|
342
|
+
"""Convert a meter string like "6/8", "C", "C|", "(2+3)/8" or "none"
|
|
343
|
+
to a (numerator, denominator) tuple, or None for free meter.
|
|
344
|
+
"""
|
|
345
|
+
meter = re.sub(r'\s+', '', meter)
|
|
346
|
+
if meter in ('none', 'free', ''):
|
|
347
|
+
return None
|
|
348
|
+
if meter == 'C':
|
|
349
|
+
return (4, 4)
|
|
350
|
+
if meter == 'C|':
|
|
351
|
+
return (2, 2)
|
|
352
|
+
m = re.match(r'\(?(\d+(?:\+\d+)*)\)?/(\d+)$', meter)
|
|
353
|
+
if m is None:
|
|
354
|
+
raise ValueError('Invalid meter "%s"' % meter)
|
|
355
|
+
num = sum(int(x) for x in m.group(1).split('+'))
|
|
356
|
+
return (num, int(m.group(2)))
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def parse_tempo(tempo):
|
|
360
|
+
"""Split a Q: field into (beat, bpm, text): Q:1/4=120 -> ((1,4), 120, '')
|
|
361
|
+
Q:120 -> (None, 120, '') Q:"Allegro" -> (None, None, 'Allegro')
|
|
362
|
+
Q:3/8 3/8 2/8=40 -> ((8,8), 40, '') (the beat is the sum).
|
|
363
|
+
"""
|
|
364
|
+
tempo = tempo.strip()
|
|
365
|
+
text = ' '.join(re.findall(r'"([^"]*)"', tempo))
|
|
366
|
+
tempo = re.sub(r'"[^"]*"', '', tempo).strip()
|
|
367
|
+
m = re.match(r'(?:((?:\d+/\d+\s*)+)=)?\s*(\d+)$', tempo)
|
|
368
|
+
if m is None:
|
|
369
|
+
return (None, None, text)
|
|
370
|
+
beat = None
|
|
371
|
+
if m.group(1) is not None:
|
|
372
|
+
total = sum(Fraction(b) for b in m.group(1).split())
|
|
373
|
+
beat = (total.numerator, total.denominator)
|
|
374
|
+
return (beat, int(m.group(2)), text)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
class TimeSignature(object):
|
|
378
|
+
def __init__(self, meter, unit_len, tempo=None):
|
|
379
|
+
self._fields = (meter, unit_len, tempo) # as written, for update()
|
|
380
|
+
self._meter = parse_meter(meter)
|
|
381
|
+
self._unit_len = [int(x) for x in (unit_len.strip() + '/1').split('/')[:2]]
|
|
382
|
+
self._tempo = parse_tempo(tempo) if tempo is not None else None
|
|
383
|
+
|
|
384
|
+
@property
|
|
385
|
+
def unit_len(self):
|
|
386
|
+
"""Unit note length as a fraction of a whole note."""
|
|
387
|
+
return self._unit_len[0] / self._unit_len[1]
|
|
388
|
+
|
|
389
|
+
def update(self, field, value):
|
|
390
|
+
"""Return (meter, unit_len, tempo) strings with one field replaced by
|
|
391
|
+
the value of an M:, L: or Q: field met in the tune body.
|
|
392
|
+
"""
|
|
393
|
+
meter, unit_len, tempo = self._fields
|
|
394
|
+
if field == 'M':
|
|
395
|
+
meter = value.strip()
|
|
396
|
+
elif field == 'L':
|
|
397
|
+
unit_len = value.strip()
|
|
398
|
+
elif field == 'Q':
|
|
399
|
+
tempo = value.strip()
|
|
400
|
+
return meter, unit_len, tempo
|
|
401
|
+
|
|
402
|
+
def __repr__(self):
|
|
403
|
+
if self._meter is None:
|
|
404
|
+
return "<TimeSignature free>"
|
|
405
|
+
return "<TimeSignature %d/%d>" % self._meter
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
# Decoration symbols from
|
|
409
|
+
# http://abcnotation.com/wiki/abc:standard:v2.1#decorations
|
|
410
|
+
symbols = """
|
|
411
|
+
!trill! "tr" (trill mark)
|
|
412
|
+
!trill(! start of an extended trill
|
|
413
|
+
!trill)! end of an extended trill
|
|
414
|
+
!lowermordent! short /|/|/ squiggle with a vertical line through it
|
|
415
|
+
!uppermordent! short /|/|/ squiggle
|
|
416
|
+
!mordent! same as !lowermordent!
|
|
417
|
+
!pralltriller! same as !uppermordent!
|
|
418
|
+
!roll! a roll mark (arc) as used in Irish music
|
|
419
|
+
!turn! a turn mark (also known as gruppetto)
|
|
420
|
+
!turnx! a turn mark with a line through it
|
|
421
|
+
!invertedturn! an inverted turn mark
|
|
422
|
+
!invertedturnx! an inverted turn mark with a line through it
|
|
423
|
+
!arpeggio! vertical squiggle
|
|
424
|
+
!>! > mark
|
|
425
|
+
!accent! same as !>!
|
|
426
|
+
!emphasis! same as !>!
|
|
427
|
+
!fermata! fermata or hold (arc above dot)
|
|
428
|
+
!invertedfermata! upside down fermata
|
|
429
|
+
!tenuto! horizontal line to indicate holding note for full duration
|
|
430
|
+
!0! - !5! fingerings
|
|
431
|
+
!+! left-hand pizzicato, or rasp for French horns
|
|
432
|
+
!plus! same as !+!
|
|
433
|
+
!snap! snap-pizzicato mark, visually similar to !thumb!
|
|
434
|
+
!slide! slide up to a note, visually similar to a half slur
|
|
435
|
+
!wedge! small filled-in wedge mark
|
|
436
|
+
!upbow! V mark
|
|
437
|
+
!downbow! squared n mark
|
|
438
|
+
!open! small circle above note indicating open string or harmonic
|
|
439
|
+
!thumb! cello thumb symbol
|
|
440
|
+
!breath! a breath mark (apostrophe-like) after note
|
|
441
|
+
!pppp! !ppp! !pp! !p! dynamics marks
|
|
442
|
+
!mp! !mf! !f! !ff! more dynamics marks
|
|
443
|
+
!fff! !ffff! !sfz! more dynamics marks
|
|
444
|
+
!crescendo(! start of a < crescendo mark
|
|
445
|
+
!<(! same as !crescendo(!
|
|
446
|
+
!crescendo)! end of a < crescendo mark, placed after the last note
|
|
447
|
+
!<)! same as !crescendo)!
|
|
448
|
+
!diminuendo(! start of a > diminuendo mark
|
|
449
|
+
!>(! same as !diminuendo(!
|
|
450
|
+
!diminuendo)! end of a > diminuendo mark, placed after the last note
|
|
451
|
+
!>)! same as !diminuendo)!
|
|
452
|
+
!segno! 2 ornate s-like symbols separated by a diagonal line
|
|
453
|
+
!coda! a ring with a cross in it
|
|
454
|
+
!D.S.! the letters D.S. (=Da Segno)
|
|
455
|
+
!D.C.! the letters D.C. (=either Da Coda or Da Capo)
|
|
456
|
+
!dacoda! the word "Da" followed by a Coda sign
|
|
457
|
+
!dacapo! the words "Da Capo"
|
|
458
|
+
!fine! the word "fine"
|
|
459
|
+
!shortphrase! vertical line on the upper part of the staff
|
|
460
|
+
!mediumphrase! same, but extending down to the centre line
|
|
461
|
+
!longphrase! same, but extending 3/4 of the way down
|
|
462
|
+
"""
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
class Token(object):
|
|
467
|
+
def __init__(self, line, char, text):
|
|
468
|
+
self._line = line
|
|
469
|
+
self._char = char
|
|
470
|
+
self._text = text
|
|
471
|
+
|
|
472
|
+
def __repr__(self):
|
|
473
|
+
return "<%s \"%s\">" % (self.__class__.__name__, self._text)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
class Note(Token):
|
|
477
|
+
def __init__(self, key, time, note, accidental, octave, num, denom, bar_accidental=None, grace=False, **kwds):
|
|
478
|
+
Token.__init__(self, **kwds)
|
|
479
|
+
self.key = key
|
|
480
|
+
self.time_sig = time
|
|
481
|
+
self.note = note
|
|
482
|
+
self.accidental = accidental # written on this note, or None
|
|
483
|
+
self.bar_accidental = bar_accidental # in force from earlier in the bar, or None
|
|
484
|
+
self.octave = octave
|
|
485
|
+
self._length = (num, denom)
|
|
486
|
+
self.tie = False # tied to the following note
|
|
487
|
+
self.grace = grace # a grace note, inside {...}
|
|
488
|
+
|
|
489
|
+
@property
|
|
490
|
+
def pitch(self):
|
|
491
|
+
"""Chromatic note value taking into account key signature and transpositions.
|
|
492
|
+
"""
|
|
493
|
+
return Pitch(self)
|
|
494
|
+
|
|
495
|
+
@property
|
|
496
|
+
def length(self):
|
|
497
|
+
n,d = self._length
|
|
498
|
+
return (int(n) if n is not None else 1, int(d) if d is not None else 1)
|
|
499
|
+
|
|
500
|
+
@length.setter
|
|
501
|
+
def length(self, value):
|
|
502
|
+
self._length = value
|
|
503
|
+
|
|
504
|
+
@property
|
|
505
|
+
def duration(self):
|
|
506
|
+
"""Length in units of the unit note length (L:)."""
|
|
507
|
+
return self.length[0] / self.length[1]
|
|
508
|
+
|
|
509
|
+
@property
|
|
510
|
+
def abs_duration(self):
|
|
511
|
+
"""Length as a fraction of a whole note."""
|
|
512
|
+
return self.duration * self.time_sig.unit_len
|
|
513
|
+
|
|
514
|
+
def dotify(self, dots, direction):
|
|
515
|
+
"""Apply dot(s) to the duration of this note.
|
|
516
|
+
"""
|
|
517
|
+
assert direction in ('left', 'right')
|
|
518
|
+
longer = direction == 'left'
|
|
519
|
+
if '<' in dots:
|
|
520
|
+
longer = not longer
|
|
521
|
+
self._length = dotted_length(self.length, len(dots), longer)
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def parse_endings(text):
|
|
526
|
+
"""Pass numbers of a variant ending: '1' -> [1] '1,3-5' -> [1, 3, 4, 5]"""
|
|
527
|
+
passes = []
|
|
528
|
+
for item in text.split(','):
|
|
529
|
+
if '-' in item:
|
|
530
|
+
lo, hi = item.split('-', 1)
|
|
531
|
+
passes.extend(range(int(lo), int(hi) + 1))
|
|
532
|
+
else:
|
|
533
|
+
passes.append(int(item))
|
|
534
|
+
return passes
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
class BarLine(Token):
|
|
538
|
+
""" | || |] [| |: :| :: |1 [2 :|2,4 """
|
|
539
|
+
def __init__(self, **kwds):
|
|
540
|
+
Token.__init__(self, **kwds)
|
|
541
|
+
m = re.match(r'([\|\[\]\:]+)(.*)', self._text)
|
|
542
|
+
bar, ending = m.groups()
|
|
543
|
+
self.repeat_start = bar.endswith(':') # |: :: :|:
|
|
544
|
+
self.repeat_end = bar.startswith(':') # :| :: :|]
|
|
545
|
+
self.endings = parse_endings(ending) if ending[:1].isdigit() else None
|
|
546
|
+
self.double = any(x in bar for x in ('||', '|]', '[|'))
|
|
547
|
+
|
|
548
|
+
Beam = BarLine # the original name of this token class
|
|
549
|
+
|
|
550
|
+
class Space(Token):
|
|
551
|
+
pass
|
|
552
|
+
|
|
553
|
+
class Slur(Token):
|
|
554
|
+
""" ( or ) """
|
|
555
|
+
pass
|
|
556
|
+
|
|
557
|
+
class Tie(Token):
|
|
558
|
+
""" - """
|
|
559
|
+
pass
|
|
560
|
+
|
|
561
|
+
class Newline(Token):
|
|
562
|
+
pass
|
|
563
|
+
|
|
564
|
+
class Continuation(Token):
|
|
565
|
+
""" \\ at end of line """
|
|
566
|
+
pass
|
|
567
|
+
|
|
568
|
+
class GracenoteBrace(Token):
|
|
569
|
+
""" { {/ or } """
|
|
570
|
+
pass
|
|
571
|
+
|
|
572
|
+
class ChordBracket(Token):
|
|
573
|
+
""" [ or ]2 (a closing bracket may carry the chord's length; it also
|
|
574
|
+
holds the chord's notes so that a following > or - applies to all) """
|
|
575
|
+
def __init__(self, notes=(), **kwds):
|
|
576
|
+
Token.__init__(self, **kwds)
|
|
577
|
+
self.notes = list(notes)
|
|
578
|
+
|
|
579
|
+
def dotify(self, dots, direction):
|
|
580
|
+
for note in self.notes:
|
|
581
|
+
note.dotify(dots, direction)
|
|
582
|
+
|
|
583
|
+
class ChordSymbol(Token):
|
|
584
|
+
""" "Amaj" """
|
|
585
|
+
pass
|
|
586
|
+
|
|
587
|
+
class Annotation(Token):
|
|
588
|
+
""" "<stuff" """
|
|
589
|
+
pass
|
|
590
|
+
|
|
591
|
+
class Decoration(Token):
|
|
592
|
+
""" .~HLMOPSTuv """
|
|
593
|
+
pass
|
|
594
|
+
|
|
595
|
+
class VoiceOverlay(Token):
|
|
596
|
+
""" & """
|
|
597
|
+
pass
|
|
598
|
+
|
|
599
|
+
class Unknown(Token):
|
|
600
|
+
""" a character the tokenizer could not interpret (strict=False only) """
|
|
601
|
+
pass
|
|
602
|
+
|
|
603
|
+
class Ignored(Token):
|
|
604
|
+
""" typesetting-only or reserved characters: y $ ` # * ; ? @ """
|
|
605
|
+
pass
|
|
606
|
+
|
|
607
|
+
class Tuplet(Token):
|
|
608
|
+
""" (3 or (p:q:r -- p notes in the time of q, applied to r notes """
|
|
609
|
+
def __init__(self, num, q=None, r=None, compound=False, **kwds):
|
|
610
|
+
Token.__init__(self, **kwds)
|
|
611
|
+
self.num = num
|
|
612
|
+
p = int(num)
|
|
613
|
+
if not q:
|
|
614
|
+
# standard 4.13: (2 and (4 and (8 are in the time of 3; (3 and (6
|
|
615
|
+
# in 2; (5 (7 (9 in 3 for compound meters, otherwise 2
|
|
616
|
+
q = {2: 3, 3: 2, 4: 3, 6: 2, 8: 3}.get(p, 3 if compound else 2)
|
|
617
|
+
self.p = p
|
|
618
|
+
self.q = int(q)
|
|
619
|
+
self.r = int(r) if r else p
|
|
620
|
+
|
|
621
|
+
class BodyField(Token):
|
|
622
|
+
pass
|
|
623
|
+
|
|
624
|
+
class InlineField(Token):
|
|
625
|
+
pass
|
|
626
|
+
|
|
627
|
+
class Rest(Token):
|
|
628
|
+
def __init__(self, symbol, num, denom, time=None, **kwds):
|
|
629
|
+
# char==X or Z means length is in measures
|
|
630
|
+
Token.__init__(self, **kwds)
|
|
631
|
+
self.symbol = symbol
|
|
632
|
+
self.time_sig = time
|
|
633
|
+
self.length = (int(num) if num is not None else 1, int(denom) if denom is not None else 1)
|
|
634
|
+
|
|
635
|
+
@property
|
|
636
|
+
def duration(self):
|
|
637
|
+
"""Length in units of the unit note length (L:), or in measures
|
|
638
|
+
for Z and X."""
|
|
639
|
+
return self.length[0] / self.length[1]
|
|
640
|
+
|
|
641
|
+
@property
|
|
642
|
+
def abs_duration(self):
|
|
643
|
+
"""Length as a fraction of a whole note."""
|
|
644
|
+
meter = self.time_sig._meter
|
|
645
|
+
if self.symbol in 'XZ' and meter is not None:
|
|
646
|
+
return self.duration * meter[0] / meter[1]
|
|
647
|
+
return self.duration * self.time_sig.unit_len
|
|
648
|
+
|
|
649
|
+
def dotify(self, dots, direction):
|
|
650
|
+
"""Apply dot(s) to the duration of this rest.
|
|
651
|
+
"""
|
|
652
|
+
assert direction in ('left', 'right')
|
|
653
|
+
longer = direction == 'left'
|
|
654
|
+
if '<' in dots:
|
|
655
|
+
longer = not longer
|
|
656
|
+
self.length = dotted_length(self.length, len(dots), longer)
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def note_length(num, slash, den):
|
|
660
|
+
"""Length (num, den) of a note or rest from the matched pieces of its
|
|
661
|
+
length string: A2 -> (2,1) A/ -> (1,2) A// -> (1,4) A3/2 -> (3,2)
|
|
662
|
+
"""
|
|
663
|
+
if den is not None:
|
|
664
|
+
den = int(den)
|
|
665
|
+
elif slash is not None:
|
|
666
|
+
den = 2 ** len(slash)
|
|
667
|
+
else:
|
|
668
|
+
den = 1
|
|
669
|
+
return (int(num) if num is not None else 1, den)
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def apply_tuplet(tuplet, notes):
|
|
673
|
+
"""Scale the length of the notes (one note or rest, or all the notes of
|
|
674
|
+
a chord, which counts as one) by q/p for the tuplet in progress.
|
|
675
|
+
Returns the tuplet state [q, p, notes left], or None once it is done.
|
|
676
|
+
"""
|
|
677
|
+
q, p, left = tuplet
|
|
678
|
+
for note in notes:
|
|
679
|
+
num, den = note.length
|
|
680
|
+
note.length = (num * q, den * p)
|
|
681
|
+
left -= 1
|
|
682
|
+
return [q, p, left] if left > 0 else None
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def dotted_length(length, n_dots, longer):
|
|
686
|
+
"""Length (num, den) after a broken rhythm of n_dots '>' or '<' marks.
|
|
687
|
+
One mark multiplies the longer side by 3/2 and the shorter by 1/2;
|
|
688
|
+
two marks give 7/4 and 1/4; and so on.
|
|
689
|
+
"""
|
|
690
|
+
num, den = length
|
|
691
|
+
if longer:
|
|
692
|
+
return (num * (2 ** (n_dots + 1) - 1), den * 2 ** n_dots)
|
|
693
|
+
return (num, den * 2 ** n_dots)
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
class InfoContext(object):
|
|
697
|
+
"""Keeps track of current information fields
|
|
698
|
+
"""
|
|
699
|
+
def __init__(self, fields):
|
|
700
|
+
self._fields = fields
|
|
701
|
+
|
|
702
|
+
def __getattr__(self, field):
|
|
703
|
+
return self._fields.get(field, None)
|
|
704
|
+
|
|
705
|
+
def copy(self, fields):
|
|
706
|
+
"""Return a copy with some fields updated
|
|
707
|
+
"""
|
|
708
|
+
f2 = InfoContext(self._fields)
|
|
709
|
+
f2._fields.update(fields)
|
|
710
|
+
return f2
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def parse_parts(spec):
|
|
714
|
+
"""Expand a P: field into the sequence of part names:
|
|
715
|
+
'ABAB' -> ['A','B','A','B'] 'A3' -> ['A','A','A']
|
|
716
|
+
'(AB)3.C' -> ['A','B','A','B','A','B','C'] (dots are ignored)
|
|
717
|
+
"""
|
|
718
|
+
def parse(i):
|
|
719
|
+
out = []
|
|
720
|
+
while i < len(spec):
|
|
721
|
+
c = spec[i]
|
|
722
|
+
if c == ')':
|
|
723
|
+
return out, i + 1
|
|
724
|
+
if c == '(':
|
|
725
|
+
sub, i = parse(i + 1)
|
|
726
|
+
elif c in '. \t':
|
|
727
|
+
i += 1
|
|
728
|
+
continue
|
|
729
|
+
else:
|
|
730
|
+
sub, i = [c], i + 1
|
|
731
|
+
m = re.match(r'\d+', spec[i:])
|
|
732
|
+
if m is not None:
|
|
733
|
+
sub = sub * int(m.group())
|
|
734
|
+
i += m.end()
|
|
735
|
+
out.extend(sub)
|
|
736
|
+
return out, i
|
|
737
|
+
return parse(0)[0]
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
class RepeatError(ValueError):
|
|
741
|
+
"""The repeat structure of a tune is ambiguous or unsupported, so its
|
|
742
|
+
playback order cannot be determined (see check_repeats())."""
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
def _colons(text):
|
|
746
|
+
"""(repeats started, repeats ended) by a bar line's colons:
|
|
747
|
+
|: (1, 0) :| (0, 1) :: (1, 1) |:: (2, 0) ::| (0, 2)
|
|
748
|
+
"""
|
|
749
|
+
if text.strip(':') == '': # ::
|
|
750
|
+
return 1, 1
|
|
751
|
+
lead = len(text) - len(text.lstrip(':'))
|
|
752
|
+
trail = len(text) - len(text.rstrip(':'))
|
|
753
|
+
return trail, lead
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def _section(bars, start):
|
|
757
|
+
"""For the repeated section beginning at bars[start], return (index of
|
|
758
|
+
the first bar after the section, number of passes, problems) where
|
|
759
|
+
problems is a list of (bar index, message) for anything that makes
|
|
760
|
+
the section's meaning ambiguous. A section ends at the next |:, at a
|
|
761
|
+
bare :|, at the end of its last ending, or (if it has no |: of its
|
|
762
|
+
own) at a double bar.
|
|
763
|
+
"""
|
|
764
|
+
problems = []
|
|
765
|
+
bl0 = bars[start][0] if isinstance(bars[start][0], BarLine) else None
|
|
766
|
+
explicit = bl0 is not None and bl0.repeat_start
|
|
767
|
+
start_passes = _colons(bl0._text)[0] + 1 if explicit else 0 # |:: repeats 3 times (4.8)
|
|
768
|
+
passes = 1
|
|
769
|
+
seen_end = has_notes = False
|
|
770
|
+
endings = [] # (bar index, ending numbers) in order
|
|
771
|
+
after_last_ending = None # first plain bar line after the last ending began
|
|
772
|
+
end = len(bars)
|
|
773
|
+
for i in range(start, len(bars)):
|
|
774
|
+
bl = bars[i][0]
|
|
775
|
+
if not isinstance(bl, BarLine):
|
|
776
|
+
has_notes = has_notes or not _is_empty(bars[i])
|
|
777
|
+
continue
|
|
778
|
+
if bl.endings is not None and not has_notes:
|
|
779
|
+
problems.append((i, 'ending at the start of a section'))
|
|
780
|
+
has_notes = has_notes or not _is_empty(bars[i])
|
|
781
|
+
if i == start:
|
|
782
|
+
if bl.endings is not None:
|
|
783
|
+
endings.append((i, bl.endings))
|
|
784
|
+
continue
|
|
785
|
+
if bl.repeat_start or (bl.repeat_end and bl.endings is None):
|
|
786
|
+
if bl.repeat_end and endings and _is_empty(bars[i]) and _starts_ending(bars, i + 1):
|
|
787
|
+
seen_end = True # |1 B :| [2 C : the next ending after a space
|
|
788
|
+
passes = max(passes, 2, start_passes)
|
|
789
|
+
continue
|
|
790
|
+
if bl.repeat_end and endings and seen_end and not bl.repeat_start:
|
|
791
|
+
# :| after the last ending: the section ends after the
|
|
792
|
+
# first bar of that ending and the :| closes a new one
|
|
793
|
+
problems.append((i, 'repeat sign after the last ending'))
|
|
794
|
+
end = after_last_ending if after_last_ending is not None else i
|
|
795
|
+
break
|
|
796
|
+
if bl.repeat_end:
|
|
797
|
+
seen_end = True
|
|
798
|
+
n = _colons(bl._text)[1] + 1
|
|
799
|
+
if n > 2 and start_passes > 2 and n != start_passes:
|
|
800
|
+
problems.append((i, 'repeat signs disagree on the number of passes'))
|
|
801
|
+
passes = max(passes, n, start_passes)
|
|
802
|
+
end = i
|
|
803
|
+
break
|
|
804
|
+
if bl.endings is not None:
|
|
805
|
+
if bl.repeat_end:
|
|
806
|
+
if not endings:
|
|
807
|
+
problems.append((i, 'ending on a repeat sign'))
|
|
808
|
+
seen_end = True
|
|
809
|
+
passes = max(passes, 2, start_passes)
|
|
810
|
+
endings.append((i, bl.endings))
|
|
811
|
+
after_last_ending = None
|
|
812
|
+
continue
|
|
813
|
+
if bl.double and (endings or not explicit):
|
|
814
|
+
# an ending ends at a double bar (4.10); with no |: a repeat
|
|
815
|
+
# restarts from the latest double bar (4.8)
|
|
816
|
+
end = i
|
|
817
|
+
break
|
|
818
|
+
if endings and after_last_ending is None:
|
|
819
|
+
after_last_ending = i
|
|
820
|
+
if explicit and not seen_end:
|
|
821
|
+
problems.append((start, 'repeat start with no repeat end'))
|
|
822
|
+
if endings:
|
|
823
|
+
numbers = [n for i, e in endings for n in e]
|
|
824
|
+
passes = max(passes, max(numbers))
|
|
825
|
+
if len(numbers) != len(set(numbers)):
|
|
826
|
+
problems.append((endings[0][0], 'ending number used twice'))
|
|
827
|
+
elif set(numbers) != set(range(1, passes + 1)):
|
|
828
|
+
problems.append((endings[0][0], 'endings %s do not cover passes 1-%d'
|
|
829
|
+
% (','.join(map(str, sorted(numbers))), passes)))
|
|
830
|
+
if [min(e) for i, e in endings] != sorted(min(e) for i, e in endings):
|
|
831
|
+
problems.append((endings[0][0], 'endings out of order'))
|
|
832
|
+
if not seen_end:
|
|
833
|
+
problems.append((endings[0][0], 'endings with no repeat sign'))
|
|
834
|
+
return end, passes, problems
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
def _starts_ending(bars, i):
|
|
838
|
+
return i < len(bars) and isinstance(bars[i][0], BarLine) and bars[i][0].endings is not None
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
def _is_empty(bar):
|
|
842
|
+
return not any(isinstance(token, (Note, Rest)) for token in bar)
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
_jump_decoration = re.compile(r'^[!+](d\.?[cs]\.?(al(coda|fine))?|dacapo|dalsegno|dacoda|segno|coda|fine)[!+]$', re.I)
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
def check_repeats(bars):
|
|
849
|
+
"""Problems that make the playback order of the bars ambiguous or
|
|
850
|
+
unsupported, as a list of (bar index, message): a |: that is never
|
|
851
|
+
closed, ending numbers that are missing, repeated or out of order,
|
|
852
|
+
endings with no repeat sign or at the start of a section, a :| after
|
|
853
|
+
the last ending, repeat signs that disagree on the number of passes,
|
|
854
|
+
a bar line made only of colons, and D.C./D.S./coda/fine marks.
|
|
855
|
+
"""
|
|
856
|
+
problems = []
|
|
857
|
+
start = 0
|
|
858
|
+
while start < len(bars):
|
|
859
|
+
end, passes, probs = _section(bars, start)
|
|
860
|
+
problems.extend(probs)
|
|
861
|
+
start = end
|
|
862
|
+
for n, bar in enumerate(bars):
|
|
863
|
+
bl = bar[0]
|
|
864
|
+
if isinstance(bl, BarLine) and bl._text.strip(':') == '' and bl._text != '::':
|
|
865
|
+
problems.append((n, 'bar line %r' % bl._text))
|
|
866
|
+
for token in bar:
|
|
867
|
+
if isinstance(token, Decoration) and _jump_decoration.match(token._text):
|
|
868
|
+
problems.append((n, '%s is not supported' % token._text))
|
|
869
|
+
return sorted(problems)
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
def expand_repeats(bars, strict=True):
|
|
873
|
+
"""The bars in playback order: each repeated section is written out
|
|
874
|
+
once per pass, and on each pass only the variant ending whose numbers
|
|
875
|
+
include that pass is played. The bars themselves are not copied.
|
|
876
|
+
|
|
877
|
+
A :| with no |: restarts from the latest of the start of the tune, a
|
|
878
|
+
double bar and the previous :| (standard 4.8). When the structure is
|
|
879
|
+
ambiguous (see check_repeats()) a RepeatError is raised; with
|
|
880
|
+
strict=False the most likely reading is used instead.
|
|
881
|
+
"""
|
|
882
|
+
if strict:
|
|
883
|
+
problems = check_repeats(bars)
|
|
884
|
+
if problems:
|
|
885
|
+
raise RepeatError('; '.join('%s (bar %d)' % (msg, i + 1) for i, msg in problems))
|
|
886
|
+
out = []
|
|
887
|
+
i = start = 0
|
|
888
|
+
pass_no = 1
|
|
889
|
+
skipped = False # the previous bar was skipped (an ending not for this pass)
|
|
890
|
+
section_end, n_pass = _section(bars, 0)[:2] if bars else (0, 1)
|
|
891
|
+
while True:
|
|
892
|
+
if i == section_end:
|
|
893
|
+
if pass_no < n_pass:
|
|
894
|
+
i, pass_no, skipped = start, pass_no + 1, False
|
|
895
|
+
continue
|
|
896
|
+
if i == len(bars):
|
|
897
|
+
break
|
|
898
|
+
start, pass_no = i, 1
|
|
899
|
+
section_end, n_pass = _section(bars, i)[:2]
|
|
900
|
+
bl = bars[i][0]
|
|
901
|
+
if isinstance(bl, BarLine):
|
|
902
|
+
# a :| ends the pass, unless we got here by skipping the ending
|
|
903
|
+
# it closes (:|[2 on a pass that does not play ending 1)
|
|
904
|
+
if bl.repeat_end and i != start and pass_no < n_pass and not skipped:
|
|
905
|
+
i, pass_no, skipped = start, pass_no + 1, False
|
|
906
|
+
continue
|
|
907
|
+
if bl.endings is not None and pass_no not in bl.endings:
|
|
908
|
+
# not this pass: skip to the next ending or the end of the section
|
|
909
|
+
i += 1
|
|
910
|
+
while i < section_end and not (isinstance(bars[i][0], BarLine) and bars[i][0].endings is not None):
|
|
911
|
+
i += 1
|
|
912
|
+
skipped = True
|
|
913
|
+
continue
|
|
914
|
+
out.append(bars[i])
|
|
915
|
+
skipped = False
|
|
916
|
+
i += 1
|
|
917
|
+
return out
|
|
918
|
+
|
|
919
|
+
|
|
920
|
+
# A problem found by Tune.validate(): 1-based line and column in the ABC
|
|
921
|
+
# text (None when it does not refer to a place) and a message.
|
|
922
|
+
Issue = namedtuple('Issue', 'line char message')
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
def split_tunes(text):
|
|
926
|
+
"""Split the text of an ABC file into one string per tune (each
|
|
927
|
+
starting at its X: field). Information fields and %% directives in the
|
|
928
|
+
file header are prepended to every tune, as the standard says they
|
|
929
|
+
apply to all of them.
|
|
930
|
+
"""
|
|
931
|
+
lines = text.split('\n')
|
|
932
|
+
starts = [i for i, line in enumerate(lines) if line.startswith('X:')]
|
|
933
|
+
if not starts:
|
|
934
|
+
return []
|
|
935
|
+
header = [l for l in lines[:starts[0]]
|
|
936
|
+
if (len(l) > 1 and l[0] in info_keys and l[1] == ':') or l.startswith('%%')]
|
|
937
|
+
tunes = []
|
|
938
|
+
for n, s in enumerate(starts):
|
|
939
|
+
e = starts[n + 1] if n + 1 < len(starts) else len(lines)
|
|
940
|
+
tunes.append('\n'.join(header + lines[s:e]))
|
|
941
|
+
return tunes
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
def validate(abc):
|
|
945
|
+
"""Problems in one tune's ABC text, as a list of Issue. The tune is
|
|
946
|
+
read with strict=False; if its header cannot be read at all, the one
|
|
947
|
+
Issue says why.
|
|
948
|
+
"""
|
|
949
|
+
try:
|
|
950
|
+
tune = Tune(abc=abc, strict=False)
|
|
951
|
+
except Exception as e:
|
|
952
|
+
return [Issue(None, None, 'cannot parse: %s' % e)]
|
|
953
|
+
return tune.validate()
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
def strip_comment(line):
|
|
957
|
+
"""Remove a % comment (and anything after it) from a line. A percent
|
|
958
|
+
escaped as \\% is not a comment.
|
|
959
|
+
"""
|
|
960
|
+
return re.split(r'(?<!\\)%', line)[0]
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
class Tune(object):
|
|
964
|
+
"""Initialize with either an ABC string or a json-parsed dict read from
|
|
965
|
+
the TheSession API.
|
|
966
|
+
"""
|
|
967
|
+
def __init__(self, abc=None, json=None, strict=True):
|
|
968
|
+
# strict: raise on anything the tokenizer cannot read. Otherwise the
|
|
969
|
+
# offending character becomes an Unknown token (see validate()).
|
|
970
|
+
self.strict = strict
|
|
971
|
+
if abc is not None:
|
|
972
|
+
self.parse_abc(abc)
|
|
973
|
+
elif json is not None:
|
|
974
|
+
self.parse_json(json)
|
|
975
|
+
else:
|
|
976
|
+
raise TypeError("must provide abc or json")
|
|
977
|
+
|
|
978
|
+
@property
|
|
979
|
+
def url(self):
|
|
980
|
+
try:
|
|
981
|
+
return "http://thesession.org/tunes/%d#setting%d" % (self.header['reference number'], self.header['setting'])
|
|
982
|
+
except KeyError:
|
|
983
|
+
return None
|
|
984
|
+
|
|
985
|
+
@property
|
|
986
|
+
def notes(self):
|
|
987
|
+
return [t for t in self.tokens if isinstance(t, Note)]
|
|
988
|
+
|
|
989
|
+
@property
|
|
990
|
+
def bars(self):
|
|
991
|
+
"""Tokens grouped by bar: a list of lists, each beginning with the
|
|
992
|
+
BarLine that opens it. Tokens before the first bar line (a pickup)
|
|
993
|
+
form a first group with no BarLine.
|
|
994
|
+
"""
|
|
995
|
+
bars = [[]]
|
|
996
|
+
for token in self.tokens:
|
|
997
|
+
if isinstance(token, BarLine):
|
|
998
|
+
bars.append([])
|
|
999
|
+
bars[-1].append(token)
|
|
1000
|
+
return [bar for bar in bars if bar]
|
|
1001
|
+
|
|
1002
|
+
@property
|
|
1003
|
+
def parts(self):
|
|
1004
|
+
"""[(part name, bars)] in source order, split where a P: field
|
|
1005
|
+
appears in the body. Bars before the first P: have the name ''.
|
|
1006
|
+
"""
|
|
1007
|
+
parts = [('', [])]
|
|
1008
|
+
pending = None
|
|
1009
|
+
for bar in self.bars:
|
|
1010
|
+
if pending is not None:
|
|
1011
|
+
parts.append((pending, []))
|
|
1012
|
+
pending = None
|
|
1013
|
+
for n, token in enumerate(bar):
|
|
1014
|
+
if isinstance(token, (BodyField, InlineField)) and token._text.strip('[]')[:2] == 'P:':
|
|
1015
|
+
name = token._text.strip('[]')[2:].strip()
|
|
1016
|
+
if n == 0:
|
|
1017
|
+
parts.append((name, [])) # P: before any music: starts here
|
|
1018
|
+
else:
|
|
1019
|
+
pending = name # after a bar line: starts at the next bar
|
|
1020
|
+
parts[-1][1].append(bar)
|
|
1021
|
+
return [p for p in parts if p[1]]
|
|
1022
|
+
|
|
1023
|
+
def validate(self):
|
|
1024
|
+
"""Problems with this tune, as a list of Issue (line, char, message).
|
|
1025
|
+
Checks: required fields, header lines that are not fields,
|
|
1026
|
+
characters the tokenizer could not read (strict=False), unbalanced
|
|
1027
|
+
chord brackets, grace braces and slurs, ties between different
|
|
1028
|
+
notes, endings without a repeat, P: parts that do not match the
|
|
1029
|
+
body, and bars whose length does not match the meter.
|
|
1030
|
+
"""
|
|
1031
|
+
issues = []
|
|
1032
|
+
|
|
1033
|
+
def at(token, message):
|
|
1034
|
+
line = self.body_lines[token._line] if token._line < len(self.body_lines) else token._line
|
|
1035
|
+
issues.append(Issue(line + 1, token._char + 1, message))
|
|
1036
|
+
|
|
1037
|
+
for name, letter in [('reference number', 'X'), ('tune title', 'T'), ('key', 'K')]:
|
|
1038
|
+
if name not in self.header:
|
|
1039
|
+
issues.append(Issue(None, None, 'missing %s field (%s:)' % (name, letter)))
|
|
1040
|
+
for n, text in self.unknown_lines:
|
|
1041
|
+
issues.append(Issue(n + 1, None, 'not an information field: %r' % text))
|
|
1042
|
+
|
|
1043
|
+
opened = {} # per line: open [ and { counts
|
|
1044
|
+
slurs = [0, 0]
|
|
1045
|
+
line = None
|
|
1046
|
+
for token in self.tokens:
|
|
1047
|
+
if token._line != line:
|
|
1048
|
+
for text, count in opened.items():
|
|
1049
|
+
if count > 0:
|
|
1050
|
+
issues.append(Issue(self.body_lines[line] + 1, None, 'unclosed %s' % text))
|
|
1051
|
+
opened, line = {'[': 0, '{': 0}, token._line
|
|
1052
|
+
if isinstance(token, Unknown):
|
|
1053
|
+
at(token, 'unexpected character %r' % token._text)
|
|
1054
|
+
elif isinstance(token, ChordBracket):
|
|
1055
|
+
if token._text == '[':
|
|
1056
|
+
opened['['] += 1
|
|
1057
|
+
elif opened['['] == 0:
|
|
1058
|
+
at(token, 'closing bracket without a chord')
|
|
1059
|
+
else:
|
|
1060
|
+
opened['['] -= 1
|
|
1061
|
+
elif isinstance(token, GracenoteBrace):
|
|
1062
|
+
if token._text != '}':
|
|
1063
|
+
opened['{'] += 1
|
|
1064
|
+
elif opened['{'] == 0:
|
|
1065
|
+
at(token, 'closing brace without grace notes')
|
|
1066
|
+
else:
|
|
1067
|
+
opened['{'] -= 1
|
|
1068
|
+
elif isinstance(token, Slur):
|
|
1069
|
+
slurs[token._text == ')'] += 1
|
|
1070
|
+
for text, count in opened.items():
|
|
1071
|
+
if count > 0 and line is not None:
|
|
1072
|
+
issues.append(Issue(self.body_lines[line] + 1, None, 'unclosed %s' % text))
|
|
1073
|
+
if slurs[0] != slurs[1]:
|
|
1074
|
+
issues.append(Issue(None, None, 'unbalanced slurs: %d opened, %d closed' % tuple(slurs)))
|
|
1075
|
+
|
|
1076
|
+
notes = self.notes
|
|
1077
|
+
for n, note in enumerate(notes[:-1]):
|
|
1078
|
+
if note.tie and (note.note, note.octave) != (notes[n + 1].note, notes[n + 1].octave):
|
|
1079
|
+
at(note, 'tie from %s to a different note %s (a slur was probably meant)'
|
|
1080
|
+
% (note._text, notes[n + 1]._text))
|
|
1081
|
+
|
|
1082
|
+
bars = self.bars
|
|
1083
|
+
for n, message in check_repeats(bars):
|
|
1084
|
+
at(bars[n][0], message)
|
|
1085
|
+
|
|
1086
|
+
order = set(parse_parts(self.header.get('parts', '')))
|
|
1087
|
+
found = set(name for name, bars in self.parts if name)
|
|
1088
|
+
for name in sorted(order - found):
|
|
1089
|
+
issues.append(Issue(None, None, 'part %s in the P: field is not in the tune' % name))
|
|
1090
|
+
for name in sorted(found - order):
|
|
1091
|
+
if order:
|
|
1092
|
+
issues.append(Issue(None, None, 'part %s in the tune is not in the P: field' % name))
|
|
1093
|
+
|
|
1094
|
+
if any(name == 'voice' for name, value in self.fields) or \
|
|
1095
|
+
any(t._text.strip('[]')[:2] == 'V:' for t in self.tokens if isinstance(t, (BodyField, InlineField))):
|
|
1096
|
+
issues.append(Issue(None, None, 'multi-voice tune: bar lengths not checked'))
|
|
1097
|
+
else:
|
|
1098
|
+
issues.extend(self._bar_length_issues())
|
|
1099
|
+
return issues
|
|
1100
|
+
|
|
1101
|
+
def _bar_length_issues(self):
|
|
1102
|
+
"""Bars whose length differs from the meter, ignoring the first and
|
|
1103
|
+
last bar (pickups) and bars with no notes."""
|
|
1104
|
+
issues = []
|
|
1105
|
+
bars = self.bars
|
|
1106
|
+
with_notes = [n for n, bar in enumerate(bars) if not _is_empty(bar)]
|
|
1107
|
+
for n, bar in enumerate(bars):
|
|
1108
|
+
if n == 0 or n == with_notes[-1] or not isinstance(bar[0], BarLine):
|
|
1109
|
+
continue
|
|
1110
|
+
length = Fraction(0)
|
|
1111
|
+
meter = first = None
|
|
1112
|
+
in_chord = False
|
|
1113
|
+
for token in bar:
|
|
1114
|
+
if isinstance(token, ChordBracket):
|
|
1115
|
+
in_chord = token._text == '['
|
|
1116
|
+
chord_counted = False
|
|
1117
|
+
elif isinstance(token, Note) and not token.grace:
|
|
1118
|
+
if in_chord:
|
|
1119
|
+
if chord_counted:
|
|
1120
|
+
continue
|
|
1121
|
+
chord_counted = True
|
|
1122
|
+
meter = token.time_sig._meter
|
|
1123
|
+
length += Fraction(*token.length) * Fraction(*token.time_sig._unit_len)
|
|
1124
|
+
elif isinstance(token, Rest):
|
|
1125
|
+
meter = token.time_sig._meter
|
|
1126
|
+
if token.symbol in 'XZ' and meter is not None:
|
|
1127
|
+
length += Fraction(*token.length) * Fraction(*meter)
|
|
1128
|
+
else:
|
|
1129
|
+
length += Fraction(*token.length) * Fraction(*token.time_sig._unit_len)
|
|
1130
|
+
else:
|
|
1131
|
+
continue
|
|
1132
|
+
if first is None:
|
|
1133
|
+
first = token
|
|
1134
|
+
if length == 0 or meter is None:
|
|
1135
|
+
continue
|
|
1136
|
+
expected = Fraction(*meter)
|
|
1137
|
+
# a short bar next to a repeat, double bar or ending is a pickup
|
|
1138
|
+
# (or the bar that a pickup completes), not an error
|
|
1139
|
+
boundary = lambda bl: bl.repeat_start or bl.repeat_end or bl.double or bl.endings is not None
|
|
1140
|
+
closing = bars[n + 1][0] if n + 1 < len(bars) else None
|
|
1141
|
+
if length < expected and (boundary(bar[0]) or (isinstance(closing, BarLine) and boundary(closing))):
|
|
1142
|
+
continue
|
|
1143
|
+
if length != expected:
|
|
1144
|
+
issues.append(Issue(self.body_lines[first._line] + 1, first._char + 1,
|
|
1145
|
+
'bar %d has length %s, expected %s' % (n + 1, length, expected)))
|
|
1146
|
+
return issues
|
|
1147
|
+
|
|
1148
|
+
def expand_repeats(self, strict=True):
|
|
1149
|
+
"""Bars in playback order (see expand_repeats()), following the part
|
|
1150
|
+
order of a P: header field when there is one. Unless strict=False,
|
|
1151
|
+
a RepeatError is raised for an ambiguous repeat structure, a P:
|
|
1152
|
+
field that does not match the body, or a multi-voice tune.
|
|
1153
|
+
"""
|
|
1154
|
+
order = parse_parts(self.header.get('parts', ''))
|
|
1155
|
+
parts = self.parts
|
|
1156
|
+
names = [name for name, bars in parts if name]
|
|
1157
|
+
if strict:
|
|
1158
|
+
if any(name == 'voice' for name, value in self.fields) or any(
|
|
1159
|
+
t._text.strip('[]')[:2] == 'V:' for t in self.tokens if isinstance(t, (BodyField, InlineField))):
|
|
1160
|
+
raise RepeatError('multi-voice tune')
|
|
1161
|
+
if order and not names:
|
|
1162
|
+
raise RepeatError('P: field in the header but no P: in the body')
|
|
1163
|
+
for name in sorted((set(order) ^ set(names)) if order else []):
|
|
1164
|
+
raise RepeatError('part %s is %s' % (name, 'not in the tune' if name in order
|
|
1165
|
+
else 'not in the P: field'))
|
|
1166
|
+
if not order or not names:
|
|
1167
|
+
return expand_repeats(self.bars, strict)
|
|
1168
|
+
expanded = {}
|
|
1169
|
+
for name, bars in parts:
|
|
1170
|
+
expanded.setdefault(name, []).extend(expand_repeats(bars, strict))
|
|
1171
|
+
out = list(expanded.get('', [])) # anything before the first P: plays once
|
|
1172
|
+
for name in order:
|
|
1173
|
+
out.extend(expanded.get(name, []))
|
|
1174
|
+
return out
|
|
1175
|
+
|
|
1176
|
+
def parse_abc(self, abc):
|
|
1177
|
+
self.abc = abc
|
|
1178
|
+
header = []
|
|
1179
|
+
tune = []
|
|
1180
|
+
self.unknown_lines = [] # (line number, text) of header lines that are not fields
|
|
1181
|
+
self.body_lines = []
|
|
1182
|
+
in_tune = False
|
|
1183
|
+
for n, line in enumerate(abc.split('\n')):
|
|
1184
|
+
# a %%directive is equivalent to an I: field (standard 11.0)
|
|
1185
|
+
if line.startswith('%%'):
|
|
1186
|
+
line = 'I:' + line[2:]
|
|
1187
|
+
line = strip_comment(line)
|
|
1188
|
+
line = line.strip()
|
|
1189
|
+
if line == '':
|
|
1190
|
+
continue
|
|
1191
|
+
if in_tune:
|
|
1192
|
+
tune.append(line)
|
|
1193
|
+
self.body_lines.append(n) # original line number of each body line
|
|
1194
|
+
else:
|
|
1195
|
+
if len(line) > 1 and line[0] in info_keys and line[1] == ':':
|
|
1196
|
+
header.append(line)
|
|
1197
|
+
if line[0] == 'K':
|
|
1198
|
+
in_tune = True
|
|
1199
|
+
elif line[:2] == '+:' and header:
|
|
1200
|
+
header[-1] += ' ' + line[2:]
|
|
1201
|
+
else:
|
|
1202
|
+
self.unknown_lines.append((n, line))
|
|
1203
|
+
|
|
1204
|
+
self.parse_header(header)
|
|
1205
|
+
self.parse_tune(tune)
|
|
1206
|
+
|
|
1207
|
+
def parse_json(self, json):
|
|
1208
|
+
self.header = {
|
|
1209
|
+
"reference number": int(json['tune_id']),
|
|
1210
|
+
"setting": int(json['setting_id']),
|
|
1211
|
+
"tune title": json['name'],
|
|
1212
|
+
"meter": json['meter'],
|
|
1213
|
+
"unit note length": "1/8", # thesession.org writes every tune with L:1/8
|
|
1214
|
+
"key": json['mode'],
|
|
1215
|
+
}
|
|
1216
|
+
self.fields = list(self.header.items())
|
|
1217
|
+
lines = json['abc'].split('\r\n')
|
|
1218
|
+
self.unknown_lines = []
|
|
1219
|
+
self.body_lines = list(range(len(lines)))
|
|
1220
|
+
self.parse_tune(lines)
|
|
1221
|
+
|
|
1222
|
+
def parse_header(self, header):
|
|
1223
|
+
# every field in order (a field may repeat, e.g. several T: or I:)
|
|
1224
|
+
self.fields = [(info_keys[line[0]].name, line[2:].strip()) for line in header]
|
|
1225
|
+
h = {}
|
|
1226
|
+
for name, data in self.fields:
|
|
1227
|
+
if name == 'tune title' and name in h:
|
|
1228
|
+
continue # the first T: is the title; later ones are alternatives
|
|
1229
|
+
h[name] = data
|
|
1230
|
+
self.header = h
|
|
1231
|
+
self.reference = h.get('reference number')
|
|
1232
|
+
self.title = h.get('tune title')
|
|
1233
|
+
self.key = h.get('key', '')
|
|
1234
|
+
|
|
1235
|
+
def parse_tune(self, tune):
|
|
1236
|
+
self.tokens = self.tokenize(tune, self.header)
|
|
1237
|
+
|
|
1238
|
+
def tokenize(self, tune, header):
|
|
1239
|
+
# get initial key signature from header
|
|
1240
|
+
key = Key(self.header.get('key', ''))
|
|
1241
|
+
|
|
1242
|
+
# get initial time signature from header
|
|
1243
|
+
meter = self.header.get('meter', 'free')
|
|
1244
|
+
unit = self.header.get('unit note length', None)
|
|
1245
|
+
# determine default unit note length from meter if possible
|
|
1246
|
+
# (standard 3.1.7: 1/16 if the meter is less than 0.75, else 1/8;
|
|
1247
|
+
# 1/8 if there is no meter)
|
|
1248
|
+
if unit is None:
|
|
1249
|
+
m = parse_meter(meter)
|
|
1250
|
+
if m is not None and m[0] / m[1] < 0.75:
|
|
1251
|
+
unit = "1/16"
|
|
1252
|
+
else:
|
|
1253
|
+
unit = "1/8"
|
|
1254
|
+
tempo = self.header.get('tempo', None)
|
|
1255
|
+
time_sig = TimeSignature(meter, unit, tempo)
|
|
1256
|
+
|
|
1257
|
+
# An accidental lasts until the end of the bar (standard 4.2). By
|
|
1258
|
+
# default it applies to the note letter in every octave ("pitch");
|
|
1259
|
+
# I:propagate-accidentals may select "octave" or "not" (11.3).
|
|
1260
|
+
propagate = 'pitch'
|
|
1261
|
+
for name, value in getattr(self, 'fields', []):
|
|
1262
|
+
if name != 'instruction':
|
|
1263
|
+
continue
|
|
1264
|
+
m = re.match(r'propagate-accidentals\s+(not|octave|pitch)', value)
|
|
1265
|
+
if m is not None:
|
|
1266
|
+
propagate = m.group(1)
|
|
1267
|
+
bar_acc = {}
|
|
1268
|
+
prev_note = None
|
|
1269
|
+
|
|
1270
|
+
tokens = []
|
|
1271
|
+
for i,line in enumerate(tune):
|
|
1272
|
+
line = strip_comment(line).rstrip()
|
|
1273
|
+
|
|
1274
|
+
if len(line) >= 2 and line[1] == ':' and (line[0] == '+' or line[0] in tune_body_fields):
|
|
1275
|
+
tokens.append(BodyField(line=i, char=0, text=line))
|
|
1276
|
+
if line[0] == 'K':
|
|
1277
|
+
key = Key(line[2:], base=key)
|
|
1278
|
+
elif line[0] in 'MLQ':
|
|
1279
|
+
meter, unit, tempo = time_sig.update(line[0], line[2:])
|
|
1280
|
+
time_sig = TimeSignature(meter, unit, tempo)
|
|
1281
|
+
continue
|
|
1282
|
+
|
|
1283
|
+
pending_dots = None
|
|
1284
|
+
chord_start = None # index of the first token inside an open [ chord
|
|
1285
|
+
tuplet = None # [q, p, notes left] while inside a tuplet
|
|
1286
|
+
in_grace = False # inside {...}
|
|
1287
|
+
j = 0
|
|
1288
|
+
while j < len(line):
|
|
1289
|
+
part = line[j:]
|
|
1290
|
+
|
|
1291
|
+
# Field
|
|
1292
|
+
if part[0] == '[' and len(part) > 3 and part[2] == ':':
|
|
1293
|
+
fields = ''.join(inline_fields.keys())
|
|
1294
|
+
m = re.match(r'\[[%s]:([^\]]+)\]' % fields, part)
|
|
1295
|
+
if m is not None:
|
|
1296
|
+
if m.group()[1] == 'K':
|
|
1297
|
+
key = Key(m.group()[3:-1], base=key)
|
|
1298
|
+
elif m.group()[1] in 'MLQ':
|
|
1299
|
+
meter, unit, tempo = time_sig.update(m.group()[1], m.group()[3:-1])
|
|
1300
|
+
time_sig = TimeSignature(meter, unit, tempo)
|
|
1301
|
+
|
|
1302
|
+
tokens.append(InlineField(line=i, char=j, text=m.group()))
|
|
1303
|
+
j += m.end()
|
|
1304
|
+
continue
|
|
1305
|
+
|
|
1306
|
+
# Space
|
|
1307
|
+
m = re.match(r'(\s+)', part)
|
|
1308
|
+
if m is not None:
|
|
1309
|
+
tokens.append(Space(line=i, char=j, text=m.group()))
|
|
1310
|
+
j += m.end()
|
|
1311
|
+
continue
|
|
1312
|
+
|
|
1313
|
+
# Note
|
|
1314
|
+
# Examples: c E' _F2 ^^G,/4 =a,',3/2
|
|
1315
|
+
m = re.match(r"(?P<acc>\^|\^\^|=|_|__)?(?P<note>[a-gA-G])(?P<oct>[,']*)(?P<num>\d+)?(?P<slash>/+)?(?P<den>\d+)?", part)
|
|
1316
|
+
if m is not None:
|
|
1317
|
+
g = m.groupdict()
|
|
1318
|
+
octave = int(g['note'].islower())
|
|
1319
|
+
if g['oct'] is not None:
|
|
1320
|
+
octave -= g['oct'].count(",")
|
|
1321
|
+
octave += g['oct'].count("'")
|
|
1322
|
+
|
|
1323
|
+
num, denom = note_length(g['num'], g['slash'], g['den'])
|
|
1324
|
+
|
|
1325
|
+
# accidental in force from earlier in the bar, or carried
|
|
1326
|
+
# over a tie from the previous note
|
|
1327
|
+
acc_key = g['note'].upper() if propagate == 'pitch' else (g['note'], octave)
|
|
1328
|
+
bar_accidental = None
|
|
1329
|
+
if g['acc'] is not None:
|
|
1330
|
+
if propagate != 'not':
|
|
1331
|
+
bar_acc[acc_key] = g['acc']
|
|
1332
|
+
else:
|
|
1333
|
+
bar_accidental = bar_acc.get(acc_key)
|
|
1334
|
+
if bar_accidental is None and prev_note is not None and prev_note.tie \
|
|
1335
|
+
and (prev_note.note, prev_note.octave) == (g['note'], octave):
|
|
1336
|
+
bar_accidental = prev_note.accidental or prev_note.bar_accidental
|
|
1337
|
+
|
|
1338
|
+
tokens.append(Note(key=key, time=time_sig, note=g['note'], accidental=g['acc'],
|
|
1339
|
+
octave=octave, num=num, denom=denom, bar_accidental=bar_accidental,
|
|
1340
|
+
grace=in_grace, line=i, char=j, text=m.group()))
|
|
1341
|
+
prev_note = tokens[-1]
|
|
1342
|
+
|
|
1343
|
+
# (inside a chord the dots and tuplet are applied at the
|
|
1344
|
+
# closing bracket, to the chord as a whole)
|
|
1345
|
+
if pending_dots is not None and chord_start is None:
|
|
1346
|
+
tokens[-1].dotify(pending_dots, 'right')
|
|
1347
|
+
pending_dots = None
|
|
1348
|
+
if tuplet is not None and chord_start is None:
|
|
1349
|
+
tuplet = apply_tuplet(tuplet, [tokens[-1]])
|
|
1350
|
+
|
|
1351
|
+
j += m.end()
|
|
1352
|
+
continue
|
|
1353
|
+
|
|
1354
|
+
# Chord brackets [ABC] [ABC]2 (a '[' that starts a bar line
|
|
1355
|
+
# or an ending, [| [1, is a bar line below)
|
|
1356
|
+
if part[0] == '[' and re.match(r'\[[\|\d]', part) is None:
|
|
1357
|
+
tokens.append(ChordBracket(line=i, char=j, text='['))
|
|
1358
|
+
chord_start = len(tokens)
|
|
1359
|
+
j += 1
|
|
1360
|
+
continue
|
|
1361
|
+
m = re.match(r"\](?P<num>\d+)?(?P<slash>/+)?(?P<den>\d+)?", part)
|
|
1362
|
+
if m is not None:
|
|
1363
|
+
g = m.groupdict()
|
|
1364
|
+
chord_notes = [] if chord_start is None else \
|
|
1365
|
+
[t for t in tokens[chord_start:] if isinstance(t, Note)]
|
|
1366
|
+
# a length on the chord multiplies the length of each note
|
|
1367
|
+
num, denom = note_length(g['num'], g['slash'], g['den'])
|
|
1368
|
+
for note in chord_notes:
|
|
1369
|
+
note._length = (note.length[0] * num, note.length[1] * denom)
|
|
1370
|
+
tokens.append(ChordBracket(notes=chord_notes, line=i, char=j, text=m.group()))
|
|
1371
|
+
chord_start = None
|
|
1372
|
+
if pending_dots is not None:
|
|
1373
|
+
tokens[-1].dotify(pending_dots, 'right')
|
|
1374
|
+
pending_dots = None
|
|
1375
|
+
if tuplet is not None:
|
|
1376
|
+
tuplet = apply_tuplet(tuplet, chord_notes)
|
|
1377
|
+
j += m.end()
|
|
1378
|
+
continue
|
|
1379
|
+
|
|
1380
|
+
# Bar line | :| |: || |] [| [|] ::
|
|
1381
|
+
# An ending may list several passes: |1 [1,3 |1-3 :|2,4
|
|
1382
|
+
m = re.match(r'((?:\[\|\]|\|\]|\[\||[\|\:])+(?:\[(?=\d))?|\[(?=\d))(\d+(?:[\-,]\d+)*|[\-,])?', part)
|
|
1383
|
+
if m is not None:
|
|
1384
|
+
tokens.append(BarLine(line=i, char=j, text=m.group()))
|
|
1385
|
+
bar_acc = {}
|
|
1386
|
+
chord_start = None # a bar line ends an unclosed chord
|
|
1387
|
+
j += m.end()
|
|
1388
|
+
continue
|
|
1389
|
+
|
|
1390
|
+
# Broken rhythm (spaces are allowed around the marks: A > B)
|
|
1391
|
+
k = len(tokens) - 1
|
|
1392
|
+
while k >= 0 and isinstance(tokens[k], Space):
|
|
1393
|
+
k -= 1
|
|
1394
|
+
if k >= 0 and isinstance(tokens[k], (Note, Rest, ChordBracket)) and chord_start is None:
|
|
1395
|
+
m = re.match('<+|>+', part)
|
|
1396
|
+
if m is not None:
|
|
1397
|
+
tokens[k].dotify(m.group(), 'left')
|
|
1398
|
+
pending_dots = m.group()
|
|
1399
|
+
j += m.end()
|
|
1400
|
+
continue
|
|
1401
|
+
|
|
1402
|
+
# Rest
|
|
1403
|
+
m = re.match(r"(?P<sym>[XZxz])(?P<num>\d+)?(?P<slash>/+)?(?P<den>\d+)?", part)
|
|
1404
|
+
if m is not None:
|
|
1405
|
+
g = m.groupdict()
|
|
1406
|
+
num, denom = note_length(g['num'], g['slash'], g['den'])
|
|
1407
|
+
tokens.append(Rest(g['sym'], num=num, denom=denom, time=time_sig, line=i, char=j, text=m.group()))
|
|
1408
|
+
|
|
1409
|
+
if pending_dots is not None:
|
|
1410
|
+
tokens[-1].dotify(pending_dots, 'right')
|
|
1411
|
+
pending_dots = None
|
|
1412
|
+
if tuplet is not None:
|
|
1413
|
+
tuplet = apply_tuplet(tuplet, [tokens[-1]])
|
|
1414
|
+
|
|
1415
|
+
j += m.end()
|
|
1416
|
+
continue
|
|
1417
|
+
|
|
1418
|
+
# Tuplets (3 (3:2:4 (must parse before slur)
|
|
1419
|
+
m = re.match(r'\(([2-9])(?::(\d*))?(?::(\d*))?', part)
|
|
1420
|
+
if m is not None:
|
|
1421
|
+
p, q, r = m.groups()
|
|
1422
|
+
meter = time_sig._meter
|
|
1423
|
+
compound = meter is not None and meter[0] % 3 == 0 and meter[0] > 3
|
|
1424
|
+
tokens.append(Tuplet(num=p, q=q, r=r, compound=compound, line=i, char=j, text=m.group()))
|
|
1425
|
+
tuplet = [tokens[-1].q, tokens[-1].p, tokens[-1].r] # scale num, scale den, notes left
|
|
1426
|
+
j += m.end()
|
|
1427
|
+
continue
|
|
1428
|
+
|
|
1429
|
+
# Slur
|
|
1430
|
+
if part[0] in '()':
|
|
1431
|
+
tokens.append(Slur(line=i, char=j, text=part[0]))
|
|
1432
|
+
j += 1
|
|
1433
|
+
continue
|
|
1434
|
+
|
|
1435
|
+
# Tie
|
|
1436
|
+
if part[0] == '-':
|
|
1437
|
+
tokens.append(Tie(line=i, char=j, text=part[0]))
|
|
1438
|
+
k = len(tokens) - 2
|
|
1439
|
+
while k >= 0 and isinstance(tokens[k], Space):
|
|
1440
|
+
k -= 1
|
|
1441
|
+
if k >= 0 and isinstance(tokens[k], Note):
|
|
1442
|
+
tokens[k].tie = True
|
|
1443
|
+
elif k >= 0 and isinstance(tokens[k], ChordBracket):
|
|
1444
|
+
for note in tokens[k].notes:
|
|
1445
|
+
note.tie = True
|
|
1446
|
+
j += 1
|
|
1447
|
+
continue
|
|
1448
|
+
|
|
1449
|
+
# Embelishments
|
|
1450
|
+
m = re.match(r'(\{[/\\]?)|\}', part)
|
|
1451
|
+
if m is not None:
|
|
1452
|
+
tokens.append(GracenoteBrace(line=i, char=j, text=m.group()))
|
|
1453
|
+
in_grace = m.group() != '}'
|
|
1454
|
+
j += m.end()
|
|
1455
|
+
continue
|
|
1456
|
+
|
|
1457
|
+
# Decorations (single character: . ~ and the letters H-W h-w,
|
|
1458
|
+
# which U: may redefine; x z are rests and y is the spacer)
|
|
1459
|
+
if part[0] in '.~HIJKLMNOPQRSTUVWhijklmnopqrstuvw':
|
|
1460
|
+
tokens.append(Decoration(line=i, char=j, text=part[0]))
|
|
1461
|
+
j += 1
|
|
1462
|
+
continue
|
|
1463
|
+
|
|
1464
|
+
# Decorations (!symbol! or the older +symbol+)
|
|
1465
|
+
m = re.match(r'\!([^\! ]+)\!|\+([^\+ ]+)\+', part)
|
|
1466
|
+
if m is not None:
|
|
1467
|
+
tokens.append(Decoration(line=i, char=j, text=m.group()))
|
|
1468
|
+
j += m.end()
|
|
1469
|
+
continue
|
|
1470
|
+
|
|
1471
|
+
# Old-style line break: a lone ! at the end of the line
|
|
1472
|
+
if part == '!':
|
|
1473
|
+
tokens.append(Ignored(line=i, char=j, text='!'))
|
|
1474
|
+
j += 1
|
|
1475
|
+
continue
|
|
1476
|
+
|
|
1477
|
+
# Voice overlay &
|
|
1478
|
+
if part[0] == '&':
|
|
1479
|
+
tokens.append(VoiceOverlay(line=i, char=j, text='&'))
|
|
1480
|
+
j += 1
|
|
1481
|
+
continue
|
|
1482
|
+
|
|
1483
|
+
# Typesetting-only symbols: y spacer, $ line break, ` beam
|
|
1484
|
+
# hint; and the reserved characters # * ; ? @, which the
|
|
1485
|
+
# standard says to ignore
|
|
1486
|
+
if part[0] in 'y$`#*;?@':
|
|
1487
|
+
tokens.append(Ignored(line=i, char=j, text=part[0]))
|
|
1488
|
+
j += 1
|
|
1489
|
+
continue
|
|
1490
|
+
|
|
1491
|
+
# Continuation
|
|
1492
|
+
if j == len(line) - 1 and line[j] == '\\':
|
|
1493
|
+
tokens.append(Continuation(line=i, char=j, text='\\'))
|
|
1494
|
+
j += 1
|
|
1495
|
+
continue
|
|
1496
|
+
|
|
1497
|
+
# Annotation
|
|
1498
|
+
m = re.match(r'"[\^\_\<\>\@][^"]+"', part)
|
|
1499
|
+
if m is not None:
|
|
1500
|
+
tokens.append(Annotation(line=i, char=j, text=m.group()))
|
|
1501
|
+
j += m.end()
|
|
1502
|
+
continue
|
|
1503
|
+
|
|
1504
|
+
# Chord symbol (anything else in quotes: "G", "D/f+", "N.C.",
|
|
1505
|
+
# "Reel." -- the standard says to handle these liberally)
|
|
1506
|
+
m = re.match(r'"[^"]*"', part)
|
|
1507
|
+
if m is not None:
|
|
1508
|
+
tokens.append(ChordSymbol(line=i, char=j, text=m.group()))
|
|
1509
|
+
j += m.end()
|
|
1510
|
+
continue
|
|
1511
|
+
|
|
1512
|
+
if self.strict:
|
|
1513
|
+
raise Exception("Unable to parse: %r (line %d, char %d)" % (part, i + 1, j + 1))
|
|
1514
|
+
tokens.append(Unknown(line=i, char=j, text=part[0]))
|
|
1515
|
+
j += 1
|
|
1516
|
+
|
|
1517
|
+
if tokens and not isinstance(tokens[-1], Continuation):
|
|
1518
|
+
tokens.append(Newline(line=i, char=j, text='\n'))
|
|
1519
|
+
|
|
1520
|
+
return tokens
|
|
1521
|
+
|
|
1522
|
+
def pitchogram(tune):
|
|
1523
|
+
hist = {}
|
|
1524
|
+
for note in tune.notes:
|
|
1525
|
+
v = note.pitch.abs_value
|
|
1526
|
+
hist[v] = hist.get(v, 0) + note.duration
|
|
1527
|
+
return hist
|