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