├── .gitignore ├── README.md ├── setup.py ├── LICENSE.txt ├── tests └── test_keys.py └── pyabc.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | tunes.json 3 | pyabc.egg-info 4 | .abc 5 | .ABC 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PyABC - Python package for parsing and analyzing ABC music notation 2 | =================================================================== 3 | 4 | Luke Campagnola, 2014 5 | 6 | 7 | Status: Pre-alpha 8 | 9 | 10 | Basic goals 11 | ----------- 12 | 13 | * Be able to parse ABC tunes using most common formatting 14 | * Analyze tunes to determine the modes and keys they use 15 | * Automatically annotate tunes with chord names 16 | * From a library of tunes, suggest sets that fit together nicely 17 | 18 | 19 | Installation 20 | ------------ 21 | You can install pyabc as a module using 22 | ```bash 23 | pip install -e /your/py/abc/directory 24 | ``` 25 | 26 | Testing 27 | ------- 28 | Limit unit testing has been implemented. To run unit tests 29 | ```bash 30 | cd /your/pyabc/directory 31 | PYTHONPATH=$PYTHONPATH:$PWD pytest 32 | ``` 33 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """A setuptools based setup module. 2 | See: 3 | https://packaging.python.org/guides/distributing-packages-using-setuptools/ 4 | https://github.com/pypa/sampleproject 5 | """ 6 | 7 | 8 | from setuptools import setup, find_packages 9 | from os import path 10 | 11 | here = path.abspath(path.dirname(__file__)) 12 | 13 | # Get the long description from the README file 14 | with open(path.join(here, 'README.md'), encoding='utf-8') as f: 15 | long_description = f.read() 16 | 17 | setup( 18 | name='pyabc', # Required 19 | version='1.0.0', # Required 20 | description='A Python Library for Parsing ABC files', 21 | long_description=long_description, 22 | long_description_content_type='text/markdown', # Optional (see note above) 23 | # @TODO url='https://github.com/pypa/sampleproject' 24 | author='Campagnola', 25 | python_requires='>3.6', 26 | # @TODO install_requires=['peppercorn'], 27 | ) 28 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2018 Luke Campagnola 2 | 3 | The MIT License 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | 10 | -------------------------------------------------------------------------------- /tests/test_keys.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for the key signature class 3 | """ 4 | 5 | import pytest 6 | import itertools 7 | 8 | from pyabc import Key 9 | 10 | 11 | def every_possible_key(): 12 | """ 13 | Create a list of every possible key that a user might send to software 14 | for use in testing. 15 | 16 | returns: 17 | A list of most of the more likely user inputs for key signature. 18 | 19 | references: 20 | http://abcnotation.com/wiki/abc:standard:v2.1#kkey 21 | """ 22 | # Base List of Keys @TODO Need to add sharps and flats 23 | keys = ['A','B', 'C', 'D', 'E', 'F', 'G', 'Bb', 'C#', 'Eb', 'F#', 'G#'] 24 | 25 | # List of Modes in Sentence case 26 | modes = ['Ionian', 'Aeolian', 'Mixolydian', 'Dorian', 'Phrygian', 27 | 'Lydian', 'Locrian', 'Major', 'Minor'] 28 | 29 | # Append upper and lower case versions of the above 30 | modes += [mode.lower() for mode in modes] +\ 31 | [mode.upper() for mode in modes] 32 | 33 | # Append truncated versions of the above 34 | modes += [mode[:3] for mode in modes] + ['m'] 35 | 36 | return [str(x[0] + x[1]) for x in itertools.product(keys, modes)] 37 | 38 | 39 | @pytest.mark.parametrize("key", every_possible_key()) 40 | def test_parse_key_basic(key): 41 | # Attempt to create key using key string provided. 42 | Key(name=key) 43 | -------------------------------------------------------------------------------- /pyabc.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | import re, sys 3 | 4 | 5 | str_type = str if sys.version > '3' else basestring 6 | 7 | 8 | # Just some tunes to test against 9 | tunes = [ 10 | """ 11 | X: 1 12 | T: The Road To Lisdoonvarna 13 | R: slide 14 | M: 12/8 15 | L: 1/8 16 | K: Edor 17 | E2B B2A B2c d2A|F2A ABA D2E FED|E2B B2A B2c d3|cdc B2A B2E E3:| 18 | |:e2f gfe d2B Bcd|c2A ABc d2B B3|e2f gfe d2B Bcd|cdc B2A B2E E3:|| 19 | """, 20 | """ 21 | X: 6 22 | T: The Kid On The Mountain 23 | R: slip jig 24 | M: 9/8 25 | L: 1/8 26 | K: Emin 27 | ~E3 FEF G2 F| ~E3 BcA BGD| ~E3 FEF G2 A| BAG FAG FED:| 28 | BGB AFD G2 D| GAB dge dBA| BGB AFA G2 A| BAG FAG FED:| 29 | ~g3 eBe e2 f|~g3 efg afd| ~g3 eBe g2 a|bag fag fed:| 30 | eB/B/B e2f ~g3|eB/B/B efg afd| eB/B/B e2f g2a|bag fag fed:| 31 | edB dBA G2D|GAB dge dBA|edB dBA G2A|BAG FAG FED:| 32 | """ 33 | ] 34 | 35 | # Information field table copied from 36 | # http://abcnotation.com/wiki/abc:standard:v2.1#abc_files_tunes_and_fragments 37 | # Columns are: 38 | # X:Field, file header, tune header, tune body, inline, type 39 | information_field_table = """ 40 | A:area yes yes no no string 41 | B:book yes yes no no string 42 | C:composer yes yes no no string 43 | D:discography yes yes no no string 44 | F:file url yes yes no no string 45 | G:group yes yes no no string 46 | H:history yes yes no no string 47 | I:instruction yes yes yes yes instruction 48 | K:key no yes yes yes instruction 49 | L:unit note length yes yes yes yes instruction 50 | M:meter yes yes yes yes instruction 51 | m:macro yes yes yes yes instruction 52 | N:notes yes yes yes yes string 53 | O:origin yes yes no no string 54 | P:parts no yes yes yes instruction 55 | Q:tempo no yes yes yes instruction 56 | R:rhythm yes yes yes yes string 57 | r:remark yes yes yes yes - 58 | S:source yes yes no no string 59 | s:symbol line no no yes no instruction 60 | T:tune title no yes yes no string 61 | U:user defined yes yes yes yes instruction 62 | V:voice no yes yes yes instruction 63 | W:words no yes yes no string 64 | w:words no no yes no string 65 | X:reference number no yes no no instruction 66 | Z:transcription yes yes no no string 67 | """ 68 | 69 | class InfoKey(object): 70 | def __init__(self, key, name, file_header, tune_header, tune_body, inline, type): 71 | self.key = key # single-letter field identifier 72 | self.name = name.strip() # information field name 73 | self.file_header = file_header=='yes' # may be used in file header 74 | self.tune_header = tune_header=='yes' # may be used in tune header 75 | self.tune_body = tune_body=='yes' # may be used in tune body 76 | self.inline = inline=='yes' # nay be used inline in tunes 77 | self.type = type.strip() # data type: string, instruction, or - 78 | 79 | # parse info field table 80 | info_keys = {} 81 | for line in information_field_table.split('\n'): 82 | if line.strip() == '': 83 | continue 84 | key = line[0] 85 | fields = re.match(r'(.*)\s+(yes|no)\s+(yes|no)\s+(yes|no)\s+(yes|no)\s+(.*)', line[2:]).groups() 86 | info_keys[key] = InfoKey(key, *fields) 87 | 88 | file_header_fields = {k:v for k,v in info_keys.items() if v.file_header} 89 | tune_header_fields = {k:v for k,v in info_keys.items() if v.tune_header} 90 | tune_body_fields = {k:v for k,v in info_keys.items() if v.tune_body} 91 | inline_fields = {k:v for k,v in info_keys.items() if v.inline} 92 | 93 | 94 | 95 | # map natural note letters to chromatic values 96 | pitch_values = {'C': 0, 'D': 2, 'E': 4, 'F': 5, 'G': 7, 'A': 9, 'B': 11, } 97 | accidental_values = {'': 0, '#': 1, 'b': -1} 98 | for n,v in list(pitch_values.items()): 99 | for a in '#b': 100 | pitch_values[n+a] = v + accidental_values[a] 101 | 102 | # map chromatic number back to most common key names 103 | chromatic_notes = ['C', 'C#', 'D', 'Eb', 'E', 'F', 'F#', 'G', 'Ab', 'A', 'Bb', 'B'] 104 | 105 | # map mode names relative to Ionian (in chromatic steps) 106 | mode_values = {'major': 0, 'minor': 3, 'ionian': 0, 'aeolian': 3, 107 | 'mixolydian': -7, 'dorian': -2, 'phrygian': -4, 'lydian': -5, 108 | 'locrian': 1} 109 | 110 | # mode name normalization 111 | mode_abbrev = {m[:3]: m for m in mode_values} 112 | 113 | # sharps/flats in ionian keys 114 | key_sig = {'C#': 7, 'F#': 6, 'B': 5, 'E': 4, 'A': 3, 'D': 2, 'G': 1, 'C': 0, 115 | 'F': -1, 'Bb': -2, 'Eb': -3, 'Ab': -4, 'Db': -5, 'Gb': -6, 'Cb': -7} 116 | sharp_order = "FCGDAEB" 117 | flat_order = "BEADGCF" 118 | 119 | 120 | class Key(object): 121 | def __init__(self, name=None, root=None, mode=None): 122 | if name is not None: 123 | self.root, self.mode = self.parse_key(name) 124 | assert root is None and mode is None 125 | else: 126 | self.root = Pitch(root) 127 | self.mode = mode 128 | 129 | def parse_key(self, key): 130 | # highland pipe keys 131 | if key in ['HP', 'Hp']: 132 | return {'F': 1, 'C': 1, 'G': 0} 133 | 134 | m = re.match(r'([A-G])(\#|b)?\s*(\w+)?(.*)', key) 135 | if m is None: 136 | raise ValueError('Invalid key "%s"' % key) 137 | base, acc, mode, extra = m.groups() 138 | if acc is None: 139 | acc = '' 140 | if mode is None: 141 | mode = 'major' 142 | if mode == 'm': 143 | mode = 'minor' 144 | try: 145 | mode = mode_abbrev[mode[:3].lower()] 146 | except KeyError: 147 | raise ValueError("Unrecognized key signature %s" % key) 148 | 149 | return Pitch(base+acc), mode 150 | 151 | @property 152 | def key_signature(self): 153 | """ 154 | List of accidentals that should be displayed in the key 155 | signature for the given key description. 156 | """ 157 | # determine number of sharps/flats for this key by first converting 158 | # to ionian, then doing the key lookup 159 | key = self.relative_ionian 160 | num_acc = key_sig[key.root.name] 161 | 162 | sig = [] 163 | # sharps or flats? 164 | if num_acc > 0: 165 | for i in range(num_acc): 166 | sig.append(sharp_order[i] + '#') 167 | else: 168 | for i in range(-num_acc): 169 | sig.append(flat_order[i] + 'b') 170 | 171 | return sig 172 | 173 | @property 174 | def accidentals(self): 175 | """A dictionary of accidentals in the key signature. 176 | """ 177 | return {p:a for p,a in self.key_signature} 178 | 179 | @property 180 | def relative_ionian(self): 181 | """ 182 | Return the ionian mode relative to the given key and mode. 183 | """ 184 | key, mode = self.root, self.mode 185 | rel = mode_values[mode] 186 | root = Pitch((key.value + rel) % 12) 187 | 188 | # Select flat or sharp to match the current key name 189 | if '#' in key.name: 190 | root2 = root.equivalent_sharp 191 | if len(root2.name) == 2: 192 | root = root2 193 | elif 'b' in key.name: 194 | root2 = root.equivalent_flat 195 | if len(root2.name) == 2: 196 | root = root2 197 | 198 | return Key(root=root, mode='ionian') 199 | 200 | def __repr__(self): 201 | return "" % (self.root.name, self.mode) 202 | 203 | 204 | class Pitch(object): 205 | def __init__(self, value, octave=None): 206 | if isinstance(value, Note): 207 | self._note = value 208 | 209 | if len(value.note) == 1: 210 | acc = value.key.accidentals.get(value.note[0].upper(), '') 211 | self._name = value.note.upper() + acc 212 | self._value = self.pitch_value(self._name) 213 | else: 214 | self._name = value.note.capitalize() 215 | self._value = self.pitch_value(value.note) 216 | 217 | assert octave is None 218 | self._octave = value.octave 219 | elif isinstance(value, str_type): 220 | self._name = value 221 | self._value = self.pitch_value(value) 222 | self._octave = octave 223 | elif isinstance(value, Pitch): 224 | self._name = value._name 225 | self._value = value._value 226 | self._octave = value._octave 227 | else: 228 | self._name = None 229 | if octave is None: 230 | self._value = value 231 | self._octave = octave 232 | else: 233 | self._value = value % 12 234 | self._octave = octave + (value // 12) 235 | 236 | def __repr__(self): 237 | return "" % self.name 238 | 239 | @property 240 | def name(self): 241 | if self._name is not None: 242 | return self._name 243 | return chromatic_notes[self.value%12] 244 | 245 | @property 246 | def value(self): 247 | return self._value 248 | 249 | @property 250 | def octave(self): 251 | return self._octave 252 | 253 | @property 254 | def abs_value(self): 255 | return self.value + self.octave * 12 256 | 257 | @staticmethod 258 | def pitch_value(pitch, root='C'): 259 | """Convert a pitch string like "A#" to a chromatic scale value relative 260 | to root. 261 | """ 262 | pitch = pitch.strip() 263 | val = pitch_values[pitch[0].upper()] 264 | for acc in pitch[1:]: 265 | val += accidental_values[acc] 266 | if root == 'C': 267 | return val 268 | return (val - Pitch.pitch_value(root)) % 12 269 | 270 | def __eq__(self, a): 271 | return self.value == a.value 272 | 273 | @property 274 | def equivalent_sharp(self): 275 | p = self - 1 276 | if len(p.name) == 1: 277 | return Pitch(p.name + '#', octave=self.octave) 278 | else: 279 | return Pitch((self-2).name + '##', octave=self.octave) 280 | 281 | @property 282 | def equivalent_flat(self): 283 | p = self + 1 284 | if len(p.name) == 1: 285 | return Pitch(p.name + 'b', octave=self.octave) 286 | else: 287 | return Pitch((self+2).name + 'bb', octave=self.octave) 288 | 289 | def __add__(self, x): 290 | return Pitch(self.value+x, octave=self.octave) 291 | 292 | def __sub__(self, x): 293 | return Pitch(self.value-x, octave=self.octave) 294 | 295 | 296 | class TimeSignature(object): 297 | def __init__(self, meter, unit_len, tempo=None): 298 | meter = meter.replace('C|', '2/2').replace('C', '4/4') 299 | self._meter = [int(x) for x in meter.split('/')] 300 | self._unit_len = [int(x) for x in unit_len.split('/')] 301 | self._tempo = tempo 302 | 303 | def __repr__(self): 304 | return "" % tuple(self._meter) 305 | 306 | 307 | # Decoration symbols from 308 | # http://abcnotation.com/wiki/abc:standard:v2.1#decorations 309 | symbols = """ 310 | !trill! "tr" (trill mark) 311 | !trill(! start of an extended trill 312 | !trill)! end of an extended trill 313 | !lowermordent! short /|/|/ squiggle with a vertical line through it 314 | !uppermordent! short /|/|/ squiggle 315 | !mordent! same as !lowermordent! 316 | !pralltriller! same as !uppermordent! 317 | !roll! a roll mark (arc) as used in Irish music 318 | !turn! a turn mark (also known as gruppetto) 319 | !turnx! a turn mark with a line through it 320 | !invertedturn! an inverted turn mark 321 | !invertedturnx! an inverted turn mark with a line through it 322 | !arpeggio! vertical squiggle 323 | !>! > mark 324 | !accent! same as !>! 325 | !emphasis! same as !>! 326 | !fermata! fermata or hold (arc above dot) 327 | !invertedfermata! upside down fermata 328 | !tenuto! horizontal line to indicate holding note for full duration 329 | !0! - !5! fingerings 330 | !+! left-hand pizzicato, or rasp for French horns 331 | !plus! same as !+! 332 | !snap! snap-pizzicato mark, visually similar to !thumb! 333 | !slide! slide up to a note, visually similar to a half slur 334 | !wedge! small filled-in wedge mark 335 | !upbow! V mark 336 | !downbow! squared n mark 337 | !open! small circle above note indicating open string or harmonic 338 | !thumb! cello thumb symbol 339 | !breath! a breath mark (apostrophe-like) after note 340 | !pppp! !ppp! !pp! !p! dynamics marks 341 | !mp! !mf! !f! !ff! more dynamics marks 342 | !fff! !ffff! !sfz! more dynamics marks 343 | !crescendo(! start of a < crescendo mark 344 | !<(! same as !crescendo(! 345 | !crescendo)! end of a < crescendo mark, placed after the last note 346 | !<)! same as !crescendo)! 347 | !diminuendo(! start of a > diminuendo mark 348 | !>(! same as !diminuendo(! 349 | !diminuendo)! end of a > diminuendo mark, placed after the last note 350 | !>)! same as !diminuendo)! 351 | !segno! 2 ornate s-like symbols separated by a diagonal line 352 | !coda! a ring with a cross in it 353 | !D.S.! the letters D.S. (=Da Segno) 354 | !D.C.! the letters D.C. (=either Da Coda or Da Capo) 355 | !dacoda! the word "Da" followed by a Coda sign 356 | !dacapo! the words "Da Capo" 357 | !fine! the word "fine" 358 | !shortphrase! vertical line on the upper part of the staff 359 | !mediumphrase! same, but extending down to the centre line 360 | !longphrase! same, but extending 3/4 of the way down 361 | """ 362 | 363 | 364 | 365 | class Token(object): 366 | def __init__(self, line, char, text): 367 | self._line = line 368 | self._char = char 369 | self._text = text 370 | 371 | def __repr__(self): 372 | return "<%s \"%s\">" % (self.__class__.__name__, self._text) 373 | 374 | 375 | class Note(Token): 376 | def __init__(self, key, time, note, accidental, octave, num, denom, **kwds): 377 | Token.__init__(self, **kwds) 378 | self.key = key 379 | self.time_sig = time 380 | self.note = note 381 | self.accidental = accidental 382 | self.octave = octave 383 | self._length = (num, denom) 384 | 385 | @property 386 | def pitch(self): 387 | """Chromatic note value taking into account key signature and transpositions. 388 | """ 389 | return Pitch(self) 390 | 391 | @property 392 | def length(self): 393 | n,d = self._length 394 | return (int(n) if n is not None else 1, int(d) if d is not None else 1) 395 | 396 | @property 397 | def duration(self): 398 | return self.length[0] / self.length[1] 399 | 400 | def dotify(self, dots, direction): 401 | """Apply dot(s) to the duration of this note. 402 | """ 403 | assert direction in ('left', 'right') 404 | longer = direction == 'left' 405 | if '<' in dots: 406 | longer = not longer 407 | n_dots = len(dots) 408 | num, den = self.length 409 | if longer: 410 | num = num * 2 + 1 411 | den = den * 2 412 | self._length = (num, den) 413 | else: 414 | den = den * 2 415 | self._length = (num, den) 416 | 417 | 418 | 419 | class Beam(Token): 420 | pass 421 | 422 | class Space(Token): 423 | pass 424 | 425 | class Slur(Token): 426 | """ ( or ) """ 427 | pass 428 | 429 | class Tie(Token): 430 | """ - """ 431 | pass 432 | 433 | class Newline(Token): 434 | pass 435 | 436 | class Continuation(Token): 437 | """ \\ at end of line """ 438 | pass 439 | 440 | class GracenoteBrace(Token): 441 | """ { {/ or } """ 442 | pass 443 | 444 | class ChordBracket(Token): 445 | """ [ or ] """ 446 | pass 447 | 448 | class ChordSymbol(Token): 449 | """ "Amaj" """ 450 | pass 451 | 452 | class Annotation(Token): 453 | """ " 2 and line[1] == ':' and (line[0] == '+' or line[0] in tune_body_fields): 591 | tokens.append(BodyField(line=i, char=0, text=line)) 592 | continue 593 | 594 | pending_dots = None 595 | j = 0 596 | while j < len(line): 597 | part = line[j:] 598 | 599 | # Field 600 | if part[0] == '[' and len(part) > 3 and part[2] == ':': 601 | fields = ''.join(inline_fields.keys()) 602 | m = re.match(r'\[[%s]:([^\]]+)\]' % fields, part) 603 | if m is not None: 604 | if m.group()[1] == 'K': 605 | key = Key(m.group()[3:-1]) 606 | 607 | tokens.append(InlineField(line=i, char=j, text=m.group())) 608 | j += m.end() 609 | continue 610 | 611 | # Space 612 | m = re.match(r'(\s+)', part) 613 | if m is not None: 614 | tokens.append(Space(line=i, char=j, text=m.group())) 615 | j += m.end() 616 | continue 617 | 618 | # Note 619 | # Examples: c E' _F2 ^^G,/4 =a,',3/2 620 | m = re.match(r"(?P\^|\^\^|=|_|__)?(?P[a-gA-G])(?P[,']*)(?P\d+)?(?P/+)?(?P\d+)?", part) 621 | if m is not None: 622 | g = m.groupdict() 623 | octave = int(g['note'].islower()) 624 | if g['oct'] is not None: 625 | octave -= g['oct'].count(",") 626 | octave += g['oct'].count("'") 627 | 628 | num = g.get('num', 1) 629 | if g['den'] is not None: 630 | denom = g['den'] 631 | elif g['slash'] is not None: 632 | denom = 2 * g['slash'].count('/') 633 | else: 634 | denom = 1 635 | 636 | tokens.append(Note(key=key, time=time_sig, note=g['note'], accidental=g['acc'], 637 | octave=octave, num=num, denom=denom, line=i, char=j, text=m.group())) 638 | 639 | if pending_dots is not None: 640 | tokens[-1].dotify(pending_dots, 'right') 641 | pending_dots = None 642 | 643 | j += m.end() 644 | continue 645 | 646 | # Beam | :| |: || and Chord [ABC] 647 | m = re.match(r'([\[\]\|\:]+)([0-9\-,])?', part) 648 | if m is not None: 649 | if m.group() in '[]': 650 | tokens.append(ChordBracket(line=i, char=j, text=m.group())) 651 | else: 652 | tokens.append(Beam(line=i, char=j, text=m.group())) 653 | j += m.end() 654 | continue 655 | 656 | # Broken rhythm 657 | if len(tokens) > 0 and isinstance(tokens[-1], (Note, Rest)): 658 | m = re.match('<+|>+', part) 659 | if m is not None: 660 | tokens[-1].dotify(part, 'left') 661 | pending_dots = part 662 | j += m.end() 663 | continue 664 | 665 | # Rest 666 | m = re.match(r'([XZxz])(\d+)?(/(\d+)?)?', part) 667 | if m is not None: 668 | g = m.groups() 669 | tokens.append(Rest(g[0], num=g[1], denom=g[3], line=i, char=j, text=m.group())) 670 | 671 | if pending_dots is not None: 672 | tokens[-1].dotify(pending_dots, 'right') 673 | pending_dots = None 674 | 675 | j += m.end() 676 | continue 677 | 678 | # Tuplets (must parse before slur) 679 | m = re.match(r'\(([2-9])', part) 680 | if m is not None: 681 | tokens.append(Tuplet(num=m.groups()[0], line=i, char=j, text=m.group())) 682 | j += m.end() 683 | continue 684 | 685 | # Slur 686 | if part[0] in '()': 687 | tokens.append(Slur(line=i, char=j, text=part[0])) 688 | j += 1 689 | continue 690 | 691 | # Tie 692 | if part[0] == '-': 693 | tokens.append(Tie(line=i, char=j, text=part[0])) 694 | j += 1 695 | continue 696 | 697 | # Embelishments 698 | m = re.match(r'(\{\\?)|\}', part) 699 | if m is not None: 700 | tokens.append(GracenoteBrace(line=i, char=j, text=m.group())) 701 | j += m.end() 702 | continue 703 | 704 | # Decorations (single character) 705 | if part[0] in '.~HLMOPSTuv': 706 | tokens.append(Decoration(line=i, char=j, text=part[0])) 707 | j += 1 708 | continue 709 | 710 | # Decorations (!symbol!) 711 | m = re.match(r'\!([^\! ]+)\!', part) 712 | if m is not None: 713 | tokens.append(Decoration(line=i, char=j, text=m.group())) 714 | j += m.end() 715 | continue 716 | 717 | # Continuation 718 | if j == len(line) - 1 and j == '\\': 719 | tokens.append(Continuation(line=i, char=j, text='\\')) 720 | j += 1 721 | continue 722 | 723 | # Annotation 724 | m = re.match(r'"[\^\_\<\>\@][^"]+"', part) 725 | if m is not None: 726 | tokens.append(Annotation(line=i, char=j, text=m.group())) 727 | j += m.end() 728 | continue 729 | 730 | # Chord symbol 731 | m = re.match(r'"[\w#/]+"', part) 732 | if m is not None: 733 | tokens.append(ChordSymbol(line=i, char=j, text=m.group())) 734 | j += m.end() 735 | continue 736 | 737 | raise Exception("Unable to parse: %s\n%s" % (part, self.url)) 738 | 739 | if not isinstance(tokens[-1], Continuation): 740 | tokens.append(Newline(line=i, char=j, text='\n')) 741 | 742 | return tokens 743 | 744 | def pitchogram(tune): 745 | hist = {} 746 | for note in tune.notes: 747 | v = note.pitch.abs_value 748 | hist[v] = hist.get(v, 0) + note.duration 749 | return hist 750 | 751 | 752 | def get_thesession_tunes(): 753 | import os, json 754 | if not os.path.isfile("tunes.json"): 755 | import sys, urllib 756 | url = 'https://raw.githubusercontent.com/adactio/TheSession-data/master/json/tunes.json' 757 | print("Downloading tunes database from %s..." % url) 758 | try: 759 | urllib.urlretrieve(url, 'tunes.json') 760 | except AttributeError: 761 | import urllib.request 762 | urllib.request.urlretrieve(url, 'tunes.json') 763 | return json.loads(open('tunes.json', 'rb').read().decode('utf8')) 764 | 765 | 766 | if __name__ == '__main__': 767 | ts_tunes = get_thesession_tunes() 768 | for i,t in enumerate(ts_tunes): 769 | print("----- %d: %s -----" % (i, t['name'])) 770 | tune = Tune(json=t) 771 | 772 | print("Header: %s" % tune.header) 773 | 774 | 775 | def show(tune): 776 | import pyqtgraph as pg 777 | plt = pg.plot() 778 | plt.addLine(y=0) 779 | plt.addLine(y=12) 780 | plt.addLine(x=0) 781 | 782 | ticks = [] 783 | for i in (0, 1): 784 | for pitch in "CDEFGAB": 785 | 786 | ticks.append((i*12 + pitch_values[pitch], pitch)) 787 | 788 | plt.getAxis('left').setTicks([ticks]) 789 | 790 | tvals = [] 791 | yvals = [] 792 | 793 | t = 0 794 | for token in tune.tokens: 795 | if isinstance(token, Beam): 796 | plt.addLine(x=t) 797 | elif isinstance(token, Note): 798 | tvals.append(t) 799 | yvals.append(token.pitch.abs_value) 800 | t += token.duration 801 | plt.plot(tvals, yvals, pen=None, symbol='o') 802 | 803 | 804 | hist = tune.pitchogram() 805 | k = sorted(hist.keys()) 806 | v = [hist[x] for x in k] 807 | plt = pg.plot() 808 | bar = pg.BarGraphItem(x=k, height=v, width=1) 809 | plt.addItem(bar) 810 | 811 | plt.getAxis('bottom').setTicks([ticks]) 812 | --------------------------------------------------------------------------------