1# -*- coding: utf-8 -*- 2# ------------------------------------------------------------------------------ 3# Name: layout.py 4# Purpose: Layout objects 5# 6# Authors: Christopher Ariza 7# Michael Scott Cuthbert 8# 9# Copyright: Copyright © 2010, 2012 Michael Scott Cuthbert and the music21 Project 10# License: BSD, see license.txt 11# ------------------------------------------------------------------------------ 12''' 13The layout.py module contains two types of objects that specify the layout on 14page (or screen) for Scores and other Stream objects. There are two main types 15of Layout objects: (1) layout describing elements and (2) layout defining Streams. 16 17(1) ScoreLayout, PageLayout, SystemLayout, and StaffLayout objects describe the size of 18pages, the geometry of page and system margins, the distance between staves, etc. 19The model for these layout objects is taken directly (perhaps too directly?) 20from MusicXML. These objects all inherit from a BaseLayout class, primarily 21as an aid to finding all of these objects as a group. ScoreLayouts give defaults 22for each page, system, and staff. Thus they contain PageLayout, SystemLayout, and 23currently one or more StaffLayout objects (but probably just one. MusicXML allows more than 24StaffLayout object because multiple staves can be in a Part. Music21 uses 25the concept of a PartStaff for a Part that is played by the same performer as another. 26e.g., the left hand of the Piano is a PartStaff paired with the right hand). 27 28PageLayout and SystemLayout objects also have a property, 'isNew', 29which if set to `True` signifies that a new page 30or system should begin here. In theory, one could define new dimensions for a page 31or system in the middle of the system or page without setting isNew to True, in 32which case these measurements would start applying on the next page. In practice, 33there's really one good place to use these Layout objects and that's in the first part 34in a score at offset 0 of the first measure on a page or system 35(or for ScoreLayout, at the beginning 36of a piece outside of any parts). But it's not an 37error to put them in other places, such as at offset 0 of the first measure of a page 38or system in all the other parts. In fact, MusicXML tends to do this, and it ends up 39not being a waste if a program extracts a single part from the middle of a score. 40 41These objects are standard :class:`~music21.base.Music21Object` types, but many 42properties such as .duration, .beat, will probably not apply. 43 44When exporting to MusicXML (which is currently the only format in which music21 can and 45does preserve these markings), many MusicXML readers will ignore these tags (or worse, 46add a new page or system when PageLayout and SystemLayout objects are found but also 47add theme wherever they want). In Finale, this behavior disappears if the MusicXML 48document notes that it <supports> new-page and new-system markings. Music21 will add 49the appropriate <supports> tags if the containing Stream has `.definesExplicitPageBreaks` 50and `.definesExplicitSystemBreaks` set to True. When importing a score that has the 51<supports> tag set, music21 will set `.definesExplicitXXXXBreaks` to True for the 52outer score and the inner parts. However, this means that if the score is manipulated 53enough that the prior layout information is obsolete, programs will need to set these 54properties to False or move the Layout tags accordingly. 55 56(2) The second set of objects are Stream subclasses that can be employed when a program 57needs to easily iterate around the systems and pages defined through the layout objects 58just described, or to get the exact position on a page (or a graphical representation 59of a page) for a particular measure or system. (Individual notes coming soon). Normal 60Score streams can be changed into LayoutStreams by calling `divideByPages(s)` on them. 61A Score that was organized: Score->Parts->Measures would then become: 62LayoutScore->Pages->Systems->Parts->Measures. 63 64The new LayoutScore has methods that enable querying what page or system a measure is in, and 65specifically where on a page a measure is (or the dimensions 66of every measure in the piece). However 67do not call .show() on a LayoutScore -- the normal score it's derived from will work just fine. 68Nor does calling .show() on a Page or System work yet, but once the LayoutStream has been created, 69code like this can be done: 70 71 s = stream.Stream(...) 72 ls = layout.divideByPages(s) 73 pg2sys3 = ls.pages[1].systems[2] # n.b.! 1, 2 74 measureStart, measureEnd = pg2sys3.measureStart, pg2sys3.measureEnd 75 scoreExcerpt = s.measures(measureStart, measureEnd) 76 scoreExcerpt.show() # will show page 2, system 3 77 78Note that while the coordinates given by music21 for a musicxml score (based on margins, 79staff size, etc.) 80generally reflect what is actually in a musicxml producer, unfortunately, x-positions are 81far less accurately 82produced by most editors. For instance, Finale scores with measure sizes that have been 83manually adjusted tend to show their 84unadjusted measure width and not their actual measure width in the MusicXML. 85 86SmartScore Pro tends to produce very good MusicXML layout data. 87''' 88 89# may need to have object to convert between size units 90import copy 91import unittest 92 93from collections import namedtuple 94 95from music21 import base 96from music21 import exceptions21 97from music21 import spanner 98from music21 import stream 99from music21.stream.enums import StaffType 100 101from music21 import environment 102_MOD = 'layout' 103environLocal = environment.Environment(_MOD) 104 105 106SystemSize = namedtuple('SystemSize', 'top left right bottom') 107PageSize = namedtuple('PageSize', 'top left right bottom width height') 108 109 110class LayoutBase(base.Music21Object): 111 ''' 112 A base class for all Layout objects, defining a classSortOrder 113 and also an inheritance tree. 114 115 >>> scoreLayout = layout.ScoreLayout() 116 >>> isinstance(scoreLayout, layout.LayoutBase) 117 True 118 ''' 119 classSortOrder = -10 120 121 def __init__(self, *args, **keywords): 122 super().__init__() 123 124 def _reprInternal(self): 125 return '' 126 127# ------------------------------------------------------------------------------ 128 129 130class ScoreLayout(LayoutBase): 131 '''Parameters for configuring a score's layout. 132 133 PageLayout objects may be found on Measure or Part Streams. 134 135 136 >>> pl = layout.PageLayout(pageNumber=4, leftMargin=234, rightMargin=124, 137 ... pageHeight=4000, pageWidth=3000, isNew=True) 138 >>> pl.pageNumber 139 4 140 >>> pl.rightMargin 141 124 142 >>> pl.leftMargin 143 234 144 >>> pl.isNew 145 True 146 147 This object represents both <print new-page> and <page-layout> 148 elements in musicxml 149 150 TODO -- make sure that the first pageLayout and systemLayout 151 for each page are working together. 152 ''' 153 154 def __init__(self, *args, **keywords): 155 super().__init__() 156 157 self.scalingMillimeters = None 158 self.scalingTenths = None 159 self.pageLayout = None 160 self.systemLayout = None 161 self.staffLayoutList = [] 162 self.appearance = None 163 self.musicFont = None 164 self.wordFont = None 165 166 for key in keywords: 167 if key.lower() == 'scalingmillimeters': 168 self.scalingMillimeters = keywords[key] 169 elif key.lower() == 'scalingtenths': 170 self.scalingTenths = keywords[key] 171 elif key.lower() == 'pagelayout': 172 self.rightMargin = keywords[key] 173 elif key.lower() == 'systemlayout': 174 self.systemLayout = keywords[key] 175 elif key.lower() == 'stafflayout': 176 self.staffLayoutList = keywords[key] 177 elif key.lower() == 'appearance': 178 self.appearance = keywords[key] 179 elif key.lower() == 'musicfont': 180 self.musicFont = keywords[key] 181 elif key.lower() == 'wordfont': 182 self.wordFont = keywords[key] 183 184 def tenthsToMillimeters(self, tenths): 185 ''' 186 given the scalingMillimeters and scalingTenths, 187 return the value in millimeters of a number of 188 musicxml "tenths" where a tenth is a tenth of the distance 189 from one staff line to another 190 191 returns 0.0 if either of scalingMillimeters or scalingTenths 192 is undefined. 193 194 195 >>> sl = layout.ScoreLayout(scalingMillimeters=2.0, scalingTenths=10) 196 >>> print(sl.tenthsToMillimeters(10)) 197 2.0 198 >>> print(sl.tenthsToMillimeters(17)) # printing to round 199 3.4 200 ''' 201 if self.scalingMillimeters is None or self.scalingTenths is None: 202 return 0.0 203 millimetersPerTenth = float(self.scalingMillimeters) / self.scalingTenths 204 return round(millimetersPerTenth * tenths, 6) 205 206 207# ------------------------------------------------------------------------------ 208class PageLayout(LayoutBase): 209 ''' 210 Parameters for configuring a page's layout. 211 212 PageLayout objects may be found on Measure or Part Streams. 213 214 215 >>> pl = layout.PageLayout(pageNumber=4, leftMargin=234, rightMargin=124, 216 ... pageHeight=4000, pageWidth=3000, isNew=True) 217 >>> pl.pageNumber 218 4 219 >>> pl.rightMargin 220 124 221 >>> pl.leftMargin 222 234 223 >>> pl.isNew 224 True 225 226 This object represents both <print new-page> and <page-layout> 227 elements in musicxml 228 229 ## TODO -- make sure that the first pageLayout and systemLayout 230 for each page are working together. 231 232 ''' 233 234 def __init__(self, *args, **keywords): 235 super().__init__() 236 237 self.pageNumber = None 238 self.leftMargin = None 239 self.rightMargin = None 240 self.topMargin = None 241 self.bottomMargin = None 242 self.pageHeight = None 243 self.pageWidth = None 244 245 # store if this is the start of a new page 246 self.isNew = None 247 248 for key in keywords: 249 if key.lower() == 'pagenumber': 250 self.pageNumber = keywords[key] 251 elif key.lower() == 'leftmargin': 252 self.leftMargin = keywords[key] 253 elif key.lower() == 'rightmargin': 254 self.rightMargin = keywords[key] 255 elif key.lower() == 'topmargin': 256 self.topMargin = keywords[key] 257 elif key.lower() == 'bottommargin': 258 self.bottomMargin = keywords[key] 259 elif key.lower() == 'pageheight': 260 self.pageHeight = keywords[key] 261 elif key.lower() == 'pagewidth': 262 self.pageWidth = keywords[key] 263 elif key.lower() == 'isnew': 264 self.isNew = keywords[key] 265 266# ------------------------------------------------------------------------------ 267 268 269class SystemLayout(LayoutBase): 270 ''' 271 Object that configures or alters a system's layout. 272 273 SystemLayout objects may be found on Measure or 274 Part Streams. 275 276 Importantly, if isNew is True then this object 277 indicates that a new system should start here. 278 279 280 >>> sl = layout.SystemLayout(leftMargin=234, rightMargin=124, distance=3, isNew=True) 281 >>> sl.distance 282 3 283 >>> sl.rightMargin 284 124 285 >>> sl.leftMargin 286 234 287 >>> sl.isNew 288 True 289 ''' 290 291 def __init__(self, *args, **keywords): 292 super().__init__() 293 294 self.leftMargin = None 295 self.rightMargin = None 296 # no top or bottom margins 297 298 # this is probably the distance between adjacent systems 299 self.distance = None 300 self.topDistance = None 301 302 # store if this is the start of a new system 303 self.isNew = None 304 305 for key in keywords: 306 if key.lower() == 'leftmargin': 307 self.leftMargin = keywords[key] 308 elif key.lower() == 'rightmargin': 309 self.rightMargin = keywords[key] 310 311 elif key.lower() == 'distance': 312 self.distance = keywords[key] 313 elif key.lower() == 'topdistance': 314 self.topDistance = keywords[key] 315 elif key.lower() == 'isnew': 316 self.isNew = keywords[key] 317 318 319class StaffLayout(LayoutBase): 320 ''' 321 Object that configures or alters the distance between 322 one staff and another in a system. 323 324 StaffLayout objects may be found on Measure or 325 Part Streams. 326 327 The musicxml equivalent <staff-layout> lives in 328 the <defaults> and in <print> attributes. 329 330 331 >>> sl = layout.StaffLayout(distance=3, staffNumber=1, staffSize=113, staffLines=5) 332 >>> sl.distance 333 3 334 335 The "number" attribute refers to which staff number 336 in a part group this refers to. Thus, it's not 337 necessary in music21, but we store it if it's there. 338 (defaults to None) 339 340 >>> sl.staffNumber 341 1 342 343 staffLines specifies the number of lines for a non 5-line staff. 344 345 >>> sl.staffLines 346 5 347 348 staffSize is a percentage of the base staff size, so 349 this defines a staff 13% larger than normal. 350 351 >>> sl.staffSize 352 113.0 353 >>> sl 354 <music21.layout.StaffLayout distance 3, staffNumber 1, staffSize 113.0, staffLines 5> 355 356 357 StaffLayout can also specify the staffType: 358 359 >>> sl.staffType = stream.enums.StaffType.OSSIA 360 361 There is one other attribute, '.hidden' which has three settings: 362 363 * None - inherit from previous StaffLayout object, or False if no object exists 364 * False - not hidden -- show as a default staff 365 * True - hidden -- for playback only staves, or for a hidden/optimized-out staff 366 367 Note: (TODO: .hidden None is not working; always gives False) 368 ''' 369 _DOC_ATTR = { 370 'staffType': ''' 371 What kind of staff is this as a stream.enums.StaffType. 372 373 >>> sl = layout.StaffLayout() 374 >>> sl.staffType 375 <StaffType.REGULAR: 'regular'> 376 >>> sl.staffType = stream.enums.StaffType.CUE 377 >>> sl.staffType 378 <StaffType.CUE: 'cue'> 379 ''', 380 } 381 def __init__(self, *args, **keywords): 382 super().__init__() 383 384 # this is the distance between adjacent staves 385 self.distance = None 386 self.staffNumber = None 387 self.staffSize = None 388 self.staffLines = None 389 self.hidden = None # True = hidden; False = shown; None = inherit 390 self.staffType: StaffType = StaffType.REGULAR 391 392 for key in keywords: 393 keyLower = key.lower() 394 if keyLower == 'distance': 395 self.distance = keywords[key] 396 elif keyLower == 'staffnumber': 397 self.staffNumber = keywords[key] 398 elif keyLower == 'staffsize': 399 if keywords[key] is not None: 400 self.staffSize = float(keywords[key]) 401 elif keyLower == 'stafflines': 402 self.staffLines = keywords[key] 403 elif keyLower == 'hidden': 404 if keywords[key] is not False and keywords[key] is not None: 405 self.hidden = True 406 elif keyLower == 'staffType': 407 self.staffType = keywords[key] 408 409 def _reprInternal(self): 410 return (f'distance {self.distance!r}, staffNumber {self.staffNumber!r}, ' 411 + f'staffSize {self.staffSize!r}, staffLines {self.staffLines!r}') 412 413# ------------------------------------------------------------------------------ 414 415 416class LayoutException(exceptions21.Music21Exception): 417 pass 418 419 420class StaffGroupException(spanner.SpannerException): 421 pass 422 423 424# ------------------------------------------------------------------------------ 425class StaffGroup(spanner.Spanner): 426 ''' 427 A StaffGroup defines a collection of one or more 428 :class:`~music21.stream.Part` objects, 429 specifying that they should be shown together with a bracket, 430 brace, or other symbol, and may have a common name. 431 432 >>> p1 = stream.Part() 433 >>> p2 = stream.Part() 434 >>> p1.append(note.Note('C5', type='whole')) 435 >>> p1.append(note.Note('D5', type='whole')) 436 >>> p2.append(note.Note('C3', type='whole')) 437 >>> p2.append(note.Note('D3', type='whole')) 438 >>> p3 = stream.Part() 439 >>> p3.append(note.Note('F#4', type='whole')) 440 >>> p3.append(note.Note('G#4', type='whole')) 441 >>> s = stream.Score() 442 >>> s.insert(0, p1) 443 >>> s.insert(0, p2) 444 >>> s.insert(0, p3) 445 >>> staffGroup1 = layout.StaffGroup([p1, p2], 446 ... name='Marimba', abbreviation='Mba.', symbol='brace') 447 >>> staffGroup1.barTogether = 'Mensurstrich' 448 >>> s.insert(0, staffGroup1) 449 >>> staffGroup2 = layout.StaffGroup([p3], 450 ... name='Xylophone', abbreviation='Xyl.', symbol='bracket') 451 >>> s.insert(0, staffGroup2) 452 >>> #_DOCS_SHOW s.show() 453 454 .. image:: images/layout_StaffGroup_01.* 455 :width: 400 456 457 ''' 458 459 def __init__(self, *arguments, **keywords): 460 super().__init__(*arguments, **keywords) 461 462 self.name = None # if this group has a name 463 self.abbreviation = None 464 self._symbol = None # can be bracket, line, brace, square 465 # determines if barlines are grouped through; this is group barline 466 # in musicxml 467 self._barTogether = True 468 469 if 'symbol' in keywords: 470 self.symbol = keywords['symbol'] # user property 471 if 'barTogether' in keywords: 472 self.barTogether = keywords['barTogether'] # user property 473 if 'name' in keywords: 474 self.name = keywords['name'] # user property 475 if 'abbreviation' in keywords: 476 self.name = keywords['abbreviation'] # user property 477 478 # -------------------------------------------------------------------------- 479 480 def _getBarTogether(self): 481 return self._barTogether 482 483 def _setBarTogether(self, value): 484 if value is None: 485 pass # do nothing for now; could set a default 486 elif value in ['yes', True]: 487 self._barTogether = True 488 elif value in ['no', False]: 489 self._barTogether = False 490 elif hasattr(value, 'lower') and value.lower() == 'mensurstrich': 491 self._barTogether = 'Mensurstrich' 492 else: 493 raise StaffGroupException(f'the bar together value {value} is not acceptable') 494 495 barTogether = property(_getBarTogether, _setBarTogether, doc=''' 496 Get or set the barTogether value, with either Boolean values 497 or yes or no strings. Or the string 'Mensurstrich' which 498 indicates barring between staves but not in staves. 499 500 Currently Mensurstrich i 501 502 503 >>> sg = layout.StaffGroup() 504 >>> sg.barTogether = 'yes' 505 >>> sg.barTogether 506 True 507 >>> sg.barTogether = 'Mensurstrich' 508 >>> sg.barTogether 509 'Mensurstrich' 510 ''') 511 512 def _getSymbol(self): 513 return self._symbol 514 515 def _setSymbol(self, value): 516 if value is None or str(value).lower() == 'none': 517 self._symbol = None 518 elif value.lower() in ['brace', 'line', 'bracket', 'square']: 519 self._symbol = value.lower() 520 else: 521 raise StaffGroupException(f'the symbol value {value} is not acceptable') 522 523 symbol = property(_getSymbol, _setSymbol, doc=''' 524 Get or set the symbol value, with either Boolean values or yes or no strings. 525 526 527 >>> sg = layout.StaffGroup() 528 >>> sg.symbol = 'Brace' 529 >>> sg.symbol 530 'brace' 531 ''') 532 533 534# --------------------------------------------------------------- 535# Stream subclasses for layout 536 537def divideByPages(scoreIn, printUpdates=False, fastMeasures=False): 538 ''' 539 Divides a score into a series of smaller scores according to page 540 breaks. Only searches for PageLayout.isNew or SystemLayout.isNew 541 on the first part. Returns a new `LayoutScore` object. 542 543 If fastMeasures is True, then the newly created System objects 544 do not have Clef signs, Key Signatures, or necessarily all the 545 applicable spanners in them. On the other hand, the position 546 (on the page) information will be just as correct with 547 fastMeasures = True and it will run much faster on large scores 548 (because our spanner gathering algorithm is currently O(n^2); 549 something TODO: to fix.) 550 551 552 >>> lt = corpus.parse('demos/layoutTest.xml') 553 >>> len(lt.parts) 554 3 555 >>> len(lt.parts[0].getElementsByClass('Measure')) 556 80 557 558 559 Divide the score up into layout.Page objects 560 561 >>> layoutScore = layout.divideByPages(lt, fastMeasures=True) 562 >>> len(layoutScore.pages) 563 4 564 >>> lastPage = layoutScore.pages[-1] 565 >>> lastPage.measureStart 566 64 567 >>> lastPage.measureEnd 568 80 569 570 the layoutScore is a subclass of stream.Opus: 571 572 >>> layoutScore 573 <music21.layout.LayoutScore ...> 574 >>> 'Opus' in layoutScore.classes 575 True 576 577 Pages are subclasses of Opus also, since they contain Scores 578 579 >>> lastPage 580 <music21.layout.Page ...> 581 >>> 'Opus' in lastPage.classes 582 True 583 584 585 Each page now has Systems not parts. 586 587 >>> firstPage = layoutScore.pages[0] 588 >>> len(firstPage.systems) 589 4 590 >>> firstSystem = firstPage.systems[0] 591 >>> firstSystem.measureStart 592 1 593 >>> firstSystem.measureEnd 594 5 595 596 Systems are a subclass of Score: 597 598 >>> firstSystem 599 <music21.layout.System ...> 600 >>> isinstance(firstSystem, stream.Score) 601 True 602 603 Each System has staves (layout.Staff objects) not parts, though Staff is a subclass of Part 604 605 >>> secondStaff = firstSystem.staves[1] 606 >>> print(len(secondStaff.getElementsByClass('Measure'))) 607 5 608 >>> secondStaff 609 <music21.layout.Staff ...> 610 >>> isinstance(secondStaff, stream.Part) 611 True 612 ''' 613 def getRichSystemLayout(inner_allSystemLayouts): 614 ''' 615 If there are multiple systemLayouts in an iterable (list or StreamIterator), 616 make a copy of the first one and get information from each successive one into 617 a rich system layout. 618 ''' 619 richestSystemLayout = copy.deepcopy(inner_allSystemLayouts[0]) 620 for sl in inner_allSystemLayouts[1:]: 621 for attribute in ('distance', 'topDistance', 'leftMargin', 'rightMargin'): 622 if (getattr(richestSystemLayout, attribute) is None 623 and getattr(sl, attribute) is not None): 624 setattr(richestSystemLayout, attribute, getattr(sl, attribute)) 625 return richestSystemLayout 626 627 pageMeasureTuples = getPageRegionMeasureNumbers(scoreIn) 628 systemMeasureTuples = getSystemRegionMeasureNumbers(scoreIn) 629 firstMeasureNumber = pageMeasureTuples[0][0] 630 lastMeasureNumber = pageMeasureTuples[-1][1] 631 632 scoreLists = LayoutScore() 633 scoreLists.definesExplicitPageBreaks = True 634 scoreLists.definesExplicitSystemBreaks = True 635 scoreLists.measureStart = firstMeasureNumber 636 scoreLists.measureEnd = lastMeasureNumber 637 for el in scoreIn: 638 if not isinstance(el, stream.Part): 639 if 'ScoreLayout' in el.classes: 640 scoreLists.scoreLayout = el 641 scoreLists.insert(scoreIn.elementOffset(el), el) 642 643 pageNumber = 0 644 systemNumber = 0 645 scoreStaffNumber = 0 646 647 for pageStartM, pageEndM in pageMeasureTuples: 648 pageNumber += 1 649 if printUpdates is True: 650 print('updating page', pageNumber) 651 thisPage = Page() 652 thisPage.measureStart = pageStartM 653 thisPage.measureEnd = pageEndM 654 thisPage.pageNumber = pageNumber 655 if fastMeasures is True: 656 thisPageAll = scoreIn.measures(pageStartM, pageEndM, collect=[], gatherSpanners=False) 657 else: 658 thisPageAll = scoreIn.measures(pageStartM, pageEndM) 659 thisPage.systemStart = systemNumber + 1 660 for el in thisPageAll: 661 if not isinstance(el.classes and 'StaffGroup' not in el, stream.Part): 662 thisPage.insert(thisPageAll.elementOffset(el), el) 663 firstMeasureOfFirstPart = thisPageAll.parts.first().getElementsByClass('Measure').first() 664 for el in firstMeasureOfFirstPart: 665 if 'PageLayout' in el.classes: 666 thisPage.pageLayout = el 667 668 pageSystemNumber = 0 669 for systemStartM, systemEndM in systemMeasureTuples: 670 if systemStartM < pageStartM or systemEndM > pageEndM: 671 continue 672 systemNumber += 1 # global, not on this page... 673 pageSystemNumber += 1 674 if fastMeasures is True: 675 measureStacks = scoreIn.measures(systemStartM, systemEndM, 676 collect=[], 677 gatherSpanners=False) 678 else: 679 measureStacks = scoreIn.measures(systemStartM, systemEndM) 680 thisSystem = System() 681 thisSystem.systemNumber = systemNumber 682 thisSystem.pageNumber = pageNumber 683 thisSystem.pageSystemNumber = pageSystemNumber 684 thisSystem.mergeAttributes(measureStacks) 685 thisSystem.elements = measureStacks 686 thisSystem.measureStart = systemStartM 687 thisSystem.measureEnd = systemEndM 688 689 systemStaffNumber = 0 690 691 for p in list(thisSystem.parts): 692 scoreStaffNumber += 1 693 systemStaffNumber += 1 694 695 staffObject = Staff() 696 staffObject.mergeAttributes(p) 697 staffObject.scoreStaffNumber = scoreStaffNumber 698 staffObject.staffNumber = systemStaffNumber 699 staffObject.pageNumber = pageNumber 700 staffObject.pageSystemNumber = pageSystemNumber 701 702 staffObject.elements = p 703 thisSystem.replace(p, staffObject) 704 allStaffLayouts = p.recurse().getElementsByClass('StaffLayout') 705 if not allStaffLayouts: 706 continue 707 # else: 708 staffObject.staffLayout = allStaffLayouts[0] 709 # if len(allStaffLayouts) > 1: 710 # print('Got many staffLayouts') 711 712 allSystemLayouts = thisSystem.recurse().getElementsByClass('SystemLayout') 713 if len(allSystemLayouts) >= 2: 714 thisSystem.systemLayout = getRichSystemLayout(allSystemLayouts) 715 elif len(allSystemLayouts) == 1: 716 thisSystem.systemLayout = allSystemLayouts[0] 717 else: 718 thisSystem.systemLayout = None 719 720 thisPage.coreAppend(thisSystem) 721 thisPage.systemEnd = systemNumber 722 thisPage.coreElementsChanged() 723 scoreLists.coreAppend(thisPage) 724 scoreLists.coreElementsChanged() 725 return scoreLists 726 727 728def getPageRegionMeasureNumbers(scoreIn): 729 return getRegionMeasureNumbers(scoreIn, 'Page') 730 731 732def getSystemRegionMeasureNumbers(scoreIn): 733 return getRegionMeasureNumbers(scoreIn, 'System') 734 735 736def getRegionMeasureNumbers(scoreIn, region='Page'): 737 ''' 738 get a list where each entry is a 2-tuplet whose first number 739 refers to the first measure on a page and whose second number 740 is the last measure on the page. 741 ''' 742 if region == 'Page': 743 classesToReturn = ['PageLayout'] 744 elif region == 'System': 745 classesToReturn = ['PageLayout', 'SystemLayout'] 746 else: 747 raise ValueError('region must be one of Page or System') 748 749 firstPart = scoreIn.parts.first() 750 # first measure could be 1 or 0 (or something else) 751 allMeasures = firstPart.getElementsByClass('Measure') 752 firstMeasureNumber = allMeasures.first().number 753 lastMeasureNumber = allMeasures.last().number 754 measureStartList = [firstMeasureNumber] 755 measureEndList = [] 756 allAppropriateLayout = firstPart.flatten().getElementsByClass(classesToReturn) 757 758 for pl in allAppropriateLayout: 759 plMeasureNumber = pl.measureNumber 760 if pl.isNew is False: 761 continue 762 if plMeasureNumber not in measureStartList: 763 # in case of firstMeasureNumber or system and page layout at same time. 764 measureStartList.append(plMeasureNumber) 765 measureEndList.append(plMeasureNumber - 1) 766 measureEndList.append(lastMeasureNumber) 767 measureList = list(zip(measureStartList, measureEndList)) 768 return measureList 769 770 771class LayoutScore(stream.Opus): 772 ''' 773 Designation that this Score is 774 divided into Pages, Systems, Staves (=Parts), 775 Measures, etc. 776 777 Used for computing location of notes, etc. 778 779 If the score does not change between calls to the various getPosition calls, 780 it is much faster as it uses a cache. 781 ''' 782 783 def __init__(self, *args, **keywords): 784 super().__init__(*args, **keywords) 785 self.scoreLayout = None 786 self.measureStart = None 787 self.measureEnd = None 788 789 @property 790 def pages(self): 791 return self.getElementsByClass(Page) 792 793 def show(self, fmt=None, app=None, **keywords): 794 ''' 795 Borrows stream.Score.show 796 797 >>> lp = layout.Page() 798 >>> ls = layout.LayoutScore() 799 >>> ls.append(lp) 800 >>> ls.show('text') 801 {0.0} <music21.layout.Page p.1> 802 <BLANKLINE> 803 ''' 804 return stream.Score.show(self, fmt=fmt, app=app, **keywords) 805 806 def getPageAndSystemNumberFromMeasureNumber(self, measureNumber): 807 ''' 808 Given a layoutScore from divideByPages and a measureNumber returns a tuple 809 of (pageId, systemId). Note that pageId is probably one less than the page number, 810 assuming that the first page number is 1, the pageId for the first page will be 0. 811 812 Similarly, the first systemId on each page will be 0 813 814 815 >>> lt = corpus.parse('demos/layoutTest.xml') 816 >>> l = layout.divideByPages(lt, fastMeasures=True) 817 >>> l.getPageAndSystemNumberFromMeasureNumber(80) 818 (3, 3) 819 ''' 820 if 'pageAndSystemNumberFromMeasureNumbers' not in self._cache: 821 self._cache['pageAndSystemNumberFromMeasureNumbers'] = {} 822 dataCache = self._cache['pageAndSystemNumberFromMeasureNumbers'] 823 824 if measureNumber in dataCache: 825 return dataCache[measureNumber] 826 827 foundPage = None 828 foundPageId = None 829 830 for pageId, thisPage in enumerate(self.pages): 831 if measureNumber < thisPage.measureStart or measureNumber > thisPage.measureEnd: 832 continue 833 foundPage = thisPage 834 foundPageId = pageId 835 break 836 837 if foundPage is None: 838 raise LayoutException('Cannot find this measure on any page!') 839 840 foundSystem = None 841 foundSystemId = None 842 for systemId, thisSystem in enumerate(foundPage.systems): 843 if measureNumber < thisSystem.measureStart or measureNumber > thisSystem.measureEnd: 844 continue 845 foundSystem = thisSystem 846 foundSystemId = systemId 847 break 848 849 if foundSystem is None: 850 raise LayoutException("that's strange, this measure was supposed to be on this page, " 851 + "but I couldn't find it anywhere!") 852 dataCache[measureNumber] = (foundPageId, foundSystemId) 853 return (foundPageId, foundSystemId) 854 855 def getMarginsAndSizeForPageId(self, pageId): 856 ''' 857 return a namedtuple of (top, left, bottom, right, width, height) 858 margins for a given pageId in tenths 859 860 Default of (100, 100, 100, 100, 850, 1100) if undefined 861 862 863 >>> #_DOCS_SHOW g = corpus.parse('luca/gloria') 864 >>> #_DOCS_SHOW m22 = g.parts[0].getElementsByClass('Measure')[22] 865 >>> #_DOCS_SHOW m22.getElementsByClass('PageLayout').first().leftMargin = 204.0 866 >>> #_DOCS_SHOW gl = layout.divideByPages(g) 867 >>> #_DOCS_SHOW gl.getMarginsAndSizeForPageId(1) 868 >>> layout.PageSize(171.0, 204.0, 171.0, 171.0, 1457.0, 1886.0) #_DOCS_HIDE 869 PageSize(top=171.0, left=204.0, right=171.0, bottom=171.0, width=1457.0, height=1886.0) 870 ''' 871 if 'marginsAndSizeForPageId' not in self._cache: 872 self._cache['marginsAndSizeForPageId'] = {} 873 dataCache = self._cache['marginsAndSizeForPageId'] 874 if pageId in dataCache: 875 return dataCache[pageId] 876 877 # define defaults 878 pageMarginTop = 100 879 pageMarginLeft = 100 880 pageMarginRight = 100 881 pageMarginBottom = 100 882 pageWidth = 850 883 pageHeight = 1100 884 885 thisPage = self.pages[pageId] 886 887 # override defaults with scoreLayout 888 if self.scoreLayout is not None: 889 scl = self.scoreLayout 890 if scl.pageLayout is not None: 891 pl = scl.pageLayout 892 if pl.pageWidth is not None: 893 pageWidth = pl.pageWidth 894 if pl.pageHeight is not None: 895 pageHeight = pl.pageHeight 896 if pl.topMargin is not None: 897 pageMarginTop = pl.topMargin 898 if pl.leftMargin is not None: 899 pageMarginLeft = pl.leftMargin 900 if pl.rightMargin is not None: 901 pageMarginRight = pl.rightMargin 902 if pl.bottomMargin is not None: 903 pageMarginBottom = pl.bottomMargin 904 905 # override global information with page specific pageLayout 906 if thisPage.pageLayout is not None: 907 pl = thisPage.pageLayout 908 if pl.pageWidth is not None: 909 pageWidth = pl.pageWidth 910 if pl.pageHeight is not None: 911 pageHeight = pl.pageHeight 912 if pl.topMargin is not None: 913 pageMarginTop = pl.topMargin 914 if pl.leftMargin is not None: 915 pageMarginLeft = pl.leftMargin 916 if pl.rightMargin is not None: 917 pageMarginRight = pl.rightMargin 918 if pl.bottomMargin is not None: 919 pageMarginBottom = pl.bottomMargin 920 921 dataTuple = PageSize(pageMarginTop, pageMarginLeft, pageMarginBottom, pageMarginRight, 922 pageWidth, pageHeight) 923 dataCache[pageId] = dataTuple 924 return dataTuple 925 926 def getPositionForSystem(self, pageId, systemId): 927 ''' 928 first systems on a page use a different positioning. 929 930 returns a Named tuple of the (top, left, right, and bottom) where each unit is 931 relative to the page margins 932 933 N.B. right is NOT the width -- it is different. It is the offset to the right margin. 934 weird, inconsistent, but most useful...bottom is the hard part to compute... 935 936 937 >>> lt = corpus.parse('demos/layoutTestMore.xml') 938 >>> ls = layout.divideByPages(lt, fastMeasures = True) 939 >>> ls.getPositionForSystem(0, 0) 940 SystemSize(top=211.0, left=70.0, right=0.0, bottom=696.0) 941 >>> ls.getPositionForSystem(0, 1) 942 SystemSize(top=810.0, left=0.0, right=0.0, bottom=1173.0) 943 >>> ls.getPositionForSystem(0, 2) 944 SystemSize(top=1340.0, left=67.0, right=92.0, bottom=1610.0) 945 >>> ls.getPositionForSystem(0, 3) 946 SystemSize(top=1724.0, left=0.0, right=0.0, bottom=2030.0) 947 >>> ls.getPositionForSystem(0, 4) 948 SystemSize(top=2144.0, left=0.0, right=0.0, bottom=2583.0) 949 ''' 950 if 'positionForSystem' not in self._cache: 951 self._cache['positionForSystem'] = {} 952 positionForSystemCache = self._cache['positionForSystem'] 953 cacheKey = f'{pageId}-{systemId}' 954 if cacheKey in positionForSystemCache: 955 return positionForSystemCache[cacheKey] 956 957 if pageId == 0 and systemId == 4: 958 pass 959 960 leftMargin = 0 961 rightMargin = 0 962 # no top or bottom margins 963 964 # distance from previous 965 previousDistance = 0 966 967 # override defaults with scoreLayout 968 if self.scoreLayout is not None: 969 scl = self.scoreLayout 970 if scl.systemLayout is not None: 971 sl = scl.systemLayout 972 if sl.leftMargin is not None: 973 leftMargin = sl.leftMargin 974 if sl.rightMargin is not None: 975 rightMargin = sl.rightMargin 976 if systemId == 0: 977 if sl.topDistance is not None: 978 previousDistance = sl.topDistance 979 else: 980 if sl.distance is not None: 981 previousDistance = sl.distance 982 983 # override global information with system specific pageLayout 984 thisSystem = self.pages[pageId].systems[systemId] 985 986 if thisSystem.systemLayout is not None: 987 sl = thisSystem.systemLayout 988 if sl.leftMargin is not None: 989 leftMargin = sl.leftMargin 990 if sl.rightMargin is not None: 991 rightMargin = sl.rightMargin 992 if systemId == 0: 993 if sl.topDistance is not None: 994 previousDistance = sl.topDistance 995 else: 996 if sl.distance is not None: 997 previousDistance = sl.distance 998 999 if systemId > 0: 1000 lastSystemDimensions = self.getPositionForSystem(pageId, systemId - 1) 1001 bottomOfLastSystem = lastSystemDimensions.bottom 1002 else: 1003 bottomOfLastSystem = 0 1004 1005 numStaves = len(thisSystem.staves) 1006 lastStaff = numStaves - 1 # 1007 unused_systemStart, systemHeight = self.getPositionForStaff(pageId, systemId, lastStaff) 1008 1009 top = previousDistance + bottomOfLastSystem 1010 bottom = top + systemHeight 1011 dataTuple = SystemSize(float(top), float(leftMargin), float(rightMargin), float(bottom)) 1012 positionForSystemCache[cacheKey] = dataTuple 1013 return dataTuple 1014 1015 def getPositionForStaff(self, pageId, systemId, staffId): 1016 ''' 1017 return a tuple of (top, bottom) for a staff, specified by a given pageId, 1018 systemId, and staffId in tenths of a staff-space. 1019 1020 This distance is specified with respect to the top of the system. 1021 1022 Staff scaling (<staff-details> in musicxml inside an <attributes> object) is 1023 taken into account, but not non 5-line staves. Thus a normally sized staff 1024 is always of height 40 (4 spaces of 10-tenths each) 1025 1026 >>> lt = corpus.parse('demos/layoutTest.xml') 1027 >>> ls = layout.divideByPages(lt, fastMeasures=True) 1028 1029 The first staff (staff 0) of each page/system always begins at height 0 and should end at 1030 height 40 if it is a 5-line staff (not taken into account) with no staffSize changes 1031 1032 >>> ls.getPositionForStaff(0, 0, 0) 1033 (0.0, 40.0) 1034 >>> ls.getPositionForStaff(1, 0, 0) 1035 (0.0, 40.0) 1036 1037 The second staff (staff 1) begins at the end of staff 0 (40.0) + 1038 the appropriate staffDistance 1039 and adds the height of the staff. Staff 1 here has a size of 80 which means 1040 80% of the normal staff size. 40 * 0.8 = 32.0: 1041 1042 >>> ls.getPositionForStaff(0, 0, 1) 1043 (133.0, 165.0) 1044 1045 The third staff (staff 2) begins after the second staff (staff 1) but is a normal 1046 size staff 1047 1048 >>> ls.getPositionForStaff(0, 0, 2) 1049 (266.0, 306.0) 1050 1051 The first staff (staff 0) of the second system (system 1) also begins at 0 1052 and as a normally-sized staff, has height of 40: 1053 1054 >>> ls.getPositionForStaff(0, 1, 0) 1055 (0.0, 40.0) 1056 1057 The spacing between the staves has changed in the second system, but the 1058 staff height has not: 1059 1060 >>> ls.getPositionForStaff(0, 1, 1) 1061 (183.0, 215.0) 1062 >>> ls.getPositionForStaff(0, 1, 2) 1063 (356.0, 396.0) 1064 1065 In the third system (system 2), the staff distance reverts to the distance 1066 of system 0, but the staffSize is now 120 or 48 tenths (40 * 1.2 = 48) 1067 1068 >>> ls.getPositionForStaff(0, 2, 1) 1069 (117.0, 165.0) 1070 1071 Page 1 (0), System 4 (3), Staff 2 (1) is a hidden ("optimized") system. 1072 Thus its staffLayout notes this: 1073 1074 >>> staffLayout031 = ls.pages[0].systems[3].staves[1].staffLayout 1075 >>> staffLayout031 1076 <music21.layout.StaffLayout distance None, staffNumber None, staffSize 80, staffLines None> 1077 >>> staffLayout031.hidden 1078 True 1079 1080 Thus, the position for this staff will have the same top and bottom, and the 1081 position for the next staff will have the same top as the previous staff: 1082 1083 >>> ls.getPositionForStaff(0, 3, 0) 1084 (0.0, 40.0) 1085 >>> ls.getPositionForStaff(0, 3, 1) 1086 (40.0, 40.0) 1087 >>> ls.getPositionForStaff(0, 3, 2) 1088 (133.0, 173.0) 1089 1090 1091 Tests for a score with PartStaff objects: 1092 >>> lt = corpus.parse('demos/layoutTestMore.xml') 1093 >>> ls = layout.divideByPages(lt, fastMeasures = True) 1094 >>> ls.getPositionForStaff(0, 0, 0) 1095 (0.0, 40.0) 1096 >>> ls.getPositionForStaff(0, 0, 1) 1097 (133.0, 173.0) 1098 >>> ls.getPositionForStaff(0, 0, 2) 1099 (235.0, 275.0) 1100 1101 >>> ls.getPositionForStaff(0, 2, 0) 1102 (0.0, 40.0) 1103 >>> ls.getPositionForStaff(0, 2, 1) 1104 (40.0, 40.0) 1105 >>> ls.getPositionForStaff(0, 2, 2) 1106 (40.0, 40.0) 1107 1108 System 4 has the top staff hidden, which has been causing problems: 1109 1110 >>> ls.getPositionForStaff(0, 4, 0) 1111 (0.0, 0.0) 1112 >>> ls.getPositionForStaff(0, 4, 1) 1113 (0.0, 40.0) 1114 ''' 1115 # if staffId == 99: 1116 # staffId = 1 1117 if 'positionForStaff' not in self._cache: 1118 self._cache['positionForStaff'] = {} 1119 positionForStaffCache = self._cache['positionForStaff'] 1120 cacheKey = f'{pageId}-{systemId}-{staffId}' 1121 if cacheKey in positionForStaffCache: 1122 return positionForStaffCache[cacheKey] 1123 1124 hiddenStaff = self.getStaffHiddenAttribute(pageId, systemId, staffId) # False 1125 if hiddenStaff is not True: 1126 staffDistanceFromPrevious = self.getStaffDistanceFromPrevious(pageId, systemId, staffId) 1127 staffHeight = self.getStaffSizeFromLayout(pageId, systemId, staffId) 1128 else: # hiddenStaff is True 1129 staffHeight = 0.0 1130 staffDistanceFromPrevious = 0.0 1131 1132 if staffId > 0: 1133 unused_previousStaffTop, previousStaffBottom = self.getPositionForStaff( 1134 pageId, systemId, staffId - 1) 1135 else: 1136 previousStaffBottom = 0 1137 1138 staffDistanceFromStart = staffDistanceFromPrevious + previousStaffBottom 1139 staffBottom = staffDistanceFromStart + staffHeight 1140 1141 dataTuple = (staffDistanceFromStart, staffBottom) 1142 positionForStaffCache[cacheKey] = dataTuple 1143 return dataTuple 1144 1145 def getStaffDistanceFromPrevious(self, pageId, systemId, staffId): 1146 ''' 1147 return the distance of this staff from the previous staff in the same system 1148 1149 for staffId = 0, this is always 0.0 1150 1151 TODO:tests, now that this is out from previous 1152 ''' 1153 if staffId == 0: 1154 return 0.0 1155 1156 if 'distanceFromPrevious' not in self._cache: 1157 self._cache['distanceFromPrevious'] = {} 1158 positionForStaffCache = self._cache['distanceFromPrevious'] 1159 cacheKey = f'{pageId}-{systemId}-{staffId}' 1160 if cacheKey in positionForStaffCache: 1161 return positionForStaffCache[cacheKey] 1162 1163 # if this is the first non-hidden staff in the score then also return 0 1164 foundVisibleStaff = False 1165 i = staffId - 1 1166 while i >= 0: 1167 hiddenStatus = self.getStaffHiddenAttribute(pageId, systemId, i) 1168 if hiddenStatus is False: 1169 foundVisibleStaff = True 1170 break 1171 else: 1172 i = i - 1 1173 if foundVisibleStaff is False: 1174 positionForStaffCache[cacheKey] = 0.0 1175 return 0.0 1176 1177 # nope, not first staff or first visible staff... 1178 1179 staffDistanceFromPrevious = 60.0 # sensible default? 1180 1181 if self.scoreLayout is not None: 1182 scl = self.scoreLayout 1183 if scl.staffLayoutList: 1184 for slTemp in scl.staffLayoutList: 1185 distanceTemp = slTemp.distance 1186 if distanceTemp is not None: 1187 staffDistanceFromPrevious = distanceTemp 1188 break 1189 1190 # override global information with staff specific pageLayout 1191 thisStaff = self.pages[pageId].systems[systemId].staves[staffId] 1192 firstMeasureOfStaff = thisStaff.getElementsByClass('Measure').first() 1193 if firstMeasureOfStaff is None: 1194 firstMeasureOfStaff = stream.Stream() 1195 environLocal.warn( 1196 f'No measures found in pageId {pageId}, systemId {systemId}, staffId {staffId}' 1197 ) 1198 1199 allStaffLayouts = firstMeasureOfStaff.getElementsByClass('StaffLayout') 1200 if allStaffLayouts: 1201 # print('Got staffLayouts: ') 1202 for slTemp in allStaffLayouts: 1203 distanceTemp = slTemp.distance 1204 if distanceTemp is not None: 1205 staffDistanceFromPrevious = distanceTemp 1206 break 1207 1208 positionForStaffCache[cacheKey] = staffDistanceFromPrevious 1209 return staffDistanceFromPrevious 1210 1211 def getStaffSizeFromLayout(self, pageId, systemId, staffId): 1212 ''' 1213 Get the currently active staff-size for a given pageId, systemId, and staffId. 1214 1215 Note that this does not take into account the hidden state of the staff, which 1216 if True makes the effective size 0.0 -- see getStaffHiddenAttribute 1217 1218 >>> lt = corpus.parse('demos/layoutTest.xml') 1219 >>> ls = layout.divideByPages(lt, fastMeasures=True) 1220 >>> ls.getStaffSizeFromLayout(0, 0, 0) 1221 40.0 1222 >>> ls.getStaffSizeFromLayout(0, 0, 1) 1223 32.0 1224 >>> ls.getStaffSizeFromLayout(0, 0, 2) 1225 40.0 1226 >>> ls.getStaffSizeFromLayout(0, 1, 1) 1227 32.0 1228 >>> ls.getStaffSizeFromLayout(0, 2, 1) 1229 48.0 1230 >>> ls.getStaffSizeFromLayout(0, 3, 1) 1231 32.0 1232 ''' 1233 if 'staffSize' not in self._cache: 1234 self._cache['staffSize'] = {} 1235 staffSizeCache = self._cache['staffSize'] 1236 cacheKey = f'{pageId}-{systemId}-{staffId}' 1237 if cacheKey in staffSizeCache: 1238 return staffSizeCache[cacheKey] 1239 1240 thisStaff = self.pages[pageId].systems[systemId].staves[staffId] 1241 firstMeasureOfStaff = thisStaff.getElementsByClass('Measure').first() 1242 if firstMeasureOfStaff is None: 1243 firstMeasureOfStaff = stream.Stream() 1244 environLocal.warn( 1245 f'No measures found in pageId {pageId}, systemId {systemId}, staffId {staffId}' 1246 ) 1247 1248 numStaffLines = 5 # TODO: should be taken from staff attributes 1249 numSpaces = numStaffLines - 1 1250 staffSizeBase = numSpaces * 10.0 1251 staffSizeDefinedLocally = False 1252 1253 staffSize = staffSizeBase 1254 1255 allStaffLayouts = list(firstMeasureOfStaff.getElementsByClass('StaffLayout')) 1256 if allStaffLayouts: 1257 # print('Got staffLayouts: ') 1258 staffLayoutObj = allStaffLayouts[0] 1259 if staffLayoutObj.staffSize is not None: 1260 staffSize = staffSizeBase * (staffLayoutObj.staffSize / 100.0) 1261 # print(f'Got staffHeight of {staffHeight} for partId {partId}') 1262 staffSizeDefinedLocally = True 1263 1264 if staffSizeDefinedLocally is False: 1265 previousPageId, previousSystemId = self.getSystemBeforeThis(pageId, systemId) 1266 if previousPageId is None: 1267 staffSize = staffSizeBase 1268 else: 1269 staffSize = self.getStaffSizeFromLayout(previousPageId, previousSystemId, staffId) 1270 1271 staffSize = float(staffSize) 1272 staffSizeCache[cacheKey] = staffSize 1273 return staffSize 1274 1275 def getStaffHiddenAttribute(self, pageId, systemId, staffId): 1276 ''' 1277 returns the staffLayout.hidden attribute for a staffId, or if it is not 1278 defined, recursively search through previous staves until one is found. 1279 1280 1281 >>> lt = corpus.parse('demos/layoutTestMore.xml') 1282 >>> ls = layout.divideByPages(lt, fastMeasures = True) 1283 >>> ls.getStaffHiddenAttribute(0, 0, 0) 1284 False 1285 >>> ls.getStaffHiddenAttribute(0, 0, 1) 1286 False 1287 >>> ls.getStaffHiddenAttribute(0, 1, 1) 1288 True 1289 >>> ls.getStaffHiddenAttribute(0, 2, 1) 1290 True 1291 >>> ls.getStaffHiddenAttribute(0, 3, 1) 1292 False 1293 ''' 1294 if 'staffHiddenAttribute' not in self._cache: 1295 self._cache['staffHiddenAttribute'] = {} 1296 1297 staffHiddenCache = self._cache['staffHiddenAttribute'] 1298 cacheKey = f'{pageId}-{systemId}-{staffId}' 1299 if cacheKey in staffHiddenCache: 1300 return staffHiddenCache[cacheKey] 1301 1302 thisStaff = self.pages[pageId].systems[systemId].staves[staffId] 1303 1304 staffLayoutObject = None 1305 allStaffLayoutObjects = list(thisStaff.flatten().getElementsByClass('StaffLayout')) 1306 if allStaffLayoutObjects: 1307 staffLayoutObject = allStaffLayoutObjects[0] 1308 if staffLayoutObject is None or staffLayoutObject.hidden is None: 1309 previousPageId, previousSystemId = self.getSystemBeforeThis(pageId, systemId) 1310 if previousPageId is None: 1311 hiddenTag = False 1312 else: 1313 hiddenTag = self.getStaffHiddenAttribute(previousPageId, previousSystemId, staffId) 1314 else: 1315 hiddenTag = staffLayoutObject.hidden 1316 1317 staffHiddenCache[cacheKey] = hiddenTag 1318 return hiddenTag 1319 1320 def getSystemBeforeThis(self, pageId, systemId): 1321 # noinspection PyShadowingNames 1322 ''' 1323 given a pageId and systemId, get the (pageId, systemId) for the previous system. 1324 1325 return (None, None) if it's the first system on the first page 1326 1327 This test score has five systems on the first page, 1328 three on the second, and two on the third 1329 1330 >>> lt = corpus.parse('demos/layoutTestMore.xml') 1331 >>> ls = layout.divideByPages(lt, fastMeasures = True) 1332 >>> systemId = 1 1333 >>> pageId = 2 # last system, last page 1334 >>> while systemId is not None: 1335 ... pageId, systemId = ls.getSystemBeforeThis(pageId, systemId) 1336 ... (pageId, systemId) 1337 (2, 0) (1, 2) (1, 1) (1, 0) (0, 4) (0, 3) (0, 2) (0, 1) (0, 0) (None, None) 1338 ''' 1339 if systemId > 0: 1340 return pageId, systemId - 1 1341 else: 1342 if pageId == 0: 1343 return (None, None) 1344 previousPageId = pageId - 1 1345 numSystems = len(self.pages[previousPageId].systems) 1346 return previousPageId, numSystems - 1 1347 1348 def getPositionForStaffMeasure(self, staffId, measureNumber, returnFormat='tenths'): 1349 ''' 1350 Given a layoutScore from divideByPages, a staffId, and a measureNumber, 1351 returns a tuple of ((top, left), (bottom, right), pageId) 1352 allowing an exact position for the measure on the page. 1353 If returnFormat is "tenths", then it will be returned in tenths. 1354 1355 If returnFormat is "float", returns each as a number from 0 to 1 where 0 is the 1356 top or left of the page, and 1 is the bottom or right of the page. 1357 1358 1359 >>> lt = corpus.parse('demos/layoutTest.xml') 1360 >>> ls = layout.divideByPages(lt, fastMeasures = True) 1361 1362 The first measure of staff one begins at 336 tenths from the top (125 for the 1363 margin top and 211 for the top-staff-distance). It begins 170.0 from the 1364 left (100 for the page-margin-left, 70 for staff-margin-left). It ends 1365 40.0 below that (staffHeight) and 247.0 to the right (measure width) 1366 1367 >>> ls.getPositionForStaffMeasure(0, 1) 1368 ((336.0, 170.0), (376.0, 417.0), 0) 1369 1370 The other staves for the same measure are below this one: 1371 1372 >>> ls.getPositionForStaffMeasure(1, 1) 1373 ((469.0, 170.0), (501.0, 417.0), 0) 1374 >>> ls.getPositionForStaffMeasure(2, 1) 1375 ((602.0, 170.0), (642.0, 417.0), 0) 1376 1377 1378 If float is requested for returning, then the numbers are the fraction of 1379 the distance across the page. 1380 1381 >>> ls.getPositionForStaffMeasure(0, 1, returnFormat='float') 1382 ((0.152..., 0.0996...), (0.170..., 0.244...), 0) 1383 1384 1385 Moving over the page boundary: 1386 1387 >>> ls.getPositionForStaffMeasure(0, 23) 1388 ((1703.0, 1345.0), (1743.0, 1606.0), 0) 1389 >>> ls.getPositionForStaffMeasure(1, 23) # hidden 1390 ((1743.0, 1345.0), (1743.0, 1606.0), 0) 1391 >>> ls.getPositionForStaffMeasure(0, 24) 1392 ((195.0, 100.0), (235.0, 431.0), 1) 1393 >>> ls.getPositionForStaffMeasure(1, 24) 1394 ((328.0, 100.0), (360.0, 431.0), 1) 1395 ''' 1396 if 'positionForPartMeasure' not in self._cache: 1397 self._cache['positionForPartMeasure'] = {} 1398 positionForPartMeasureCache = self._cache['positionForPartMeasure'] 1399 if measureNumber not in positionForPartMeasureCache: 1400 positionForPartMeasureCache[measureNumber] = {} 1401 dataCache = positionForPartMeasureCache[measureNumber] 1402 if staffId in dataCache: 1403 return dataCache[staffId] 1404 1405 pageId, systemId = self.getPageAndSystemNumberFromMeasureNumber(measureNumber) 1406 1407 startXMeasure, endXMeasure = self.measurePositionWithinSystem( 1408 measureNumber, pageId, systemId) 1409 staffTop, staffBottom = self.getPositionForStaff(pageId, systemId, staffId) 1410 systemPos = self.getPositionForSystem(pageId, systemId) 1411 systemTop = systemPos.top 1412 systemLeft = systemPos.left 1413 pageSize = self.getMarginsAndSizeForPageId(pageId) 1414 1415 top = pageSize.top + systemTop + staffTop 1416 left = pageSize.left + systemLeft + startXMeasure 1417 bottom = pageSize.top + systemTop + staffBottom 1418 right = pageSize.left + systemLeft + endXMeasure 1419 pageWidth = pageSize.width 1420 pageHeight = pageSize.height 1421 1422 dataTuple = None 1423 if returnFormat == 'tenths': 1424 dataTuple = ((top, left), (bottom, right), pageId) 1425 else: 1426 pageWidth = float(pageWidth) 1427 pageHeight = float(pageHeight) 1428 topRatio = float(top) / pageHeight 1429 leftRatio = float(left) / pageWidth 1430 bottomRatio = float(bottom) / pageHeight 1431 rightRatio = float(right) / pageWidth 1432 dataTuple = ((topRatio, leftRatio), (bottomRatio, rightRatio), pageId) 1433 1434 dataCache[staffId] = dataTuple 1435 return dataTuple 1436 # return self.getPositionForStaffIdSystemIdPageIdMeasure( 1437 # staffId, systemId, pageId, measureNumber, returnFormat) 1438 1439 def measurePositionWithinSystem(self, measureNumber, pageId=None, systemId=None): 1440 ''' 1441 Given a measure number, find the start and end X positions (with respect to 1442 the system margins) for the measure. 1443 1444 if pageId and systemId are given, then it will speed up the search. But not necessary 1445 1446 no staffId is needed since (at least for now) all measures begin and end at the same 1447 X position 1448 1449 1450 >>> l = corpus.parse('demos/layoutTest.xml') 1451 >>> ls = layout.divideByPages(l, fastMeasures = True) 1452 >>> ls.measurePositionWithinSystem(1, 0, 0) 1453 (0.0, 247.0) 1454 >>> ls.measurePositionWithinSystem(2, 0, 0) 1455 (247.0, 544.0) 1456 >>> ls.measurePositionWithinSystem(3, 0, 0) 1457 (544.0, 841.0) 1458 1459 Measure positions reset at the start of a new system 1460 1461 >>> ls.measurePositionWithinSystem(6) 1462 (0.0, 331.0) 1463 >>> ls.measurePositionWithinSystem(7) 1464 (331.0, 549.0) 1465 ''' 1466 if pageId is None or systemId is None: 1467 pageId, systemId = self.getPageAndSystemNumberFromMeasureNumber(measureNumber) 1468 1469 thisSystem = self.pages[pageId].systems[systemId] 1470 startOffset = 0.0 1471 width = None 1472 thisSystemStaves = thisSystem.staves 1473 measureStream = thisSystemStaves[0].getElementsByClass('Measure') 1474 for i, m in enumerate(measureStream): 1475 currentWidth = m.layoutWidth 1476 if currentWidth is None: 1477 # first system is hidden, thus has no width information 1478 for j in range(1, len(thisSystemStaves)): 1479 searchOtherStaffForWidth = thisSystemStaves[j] 1480 searchIter = searchOtherStaffForWidth.iter() 1481 searchOtherStaffMeasure = searchIter.getElementsByClass('Measure')[i] 1482 if searchOtherStaffMeasure.layoutWidth is not None: 1483 currentWidth = searchOtherStaffMeasure.layoutWidth 1484 break 1485 if currentWidth is None: 1486 # error mode? throw error? or assume default width? Let's do the latter for now 1487 environLocal.warn( 1488 f'Could not get width for measure {m.number}, using default of 300') 1489 currentWidth = 300.0 1490 else: 1491 currentWidth = float(currentWidth) 1492 if m.number == measureNumber: 1493 width = currentWidth 1494 break 1495 else: 1496 startOffset += currentWidth 1497 1498 return startOffset, startOffset + width 1499 1500 def getAllMeasurePositionsInDocument(self, returnFormat='tenths', printUpdates=False): 1501 ''' 1502 returns a list of dictionaries, where each dictionary gives the measure number 1503 and other information, etc. in the document. 1504 1505 1506 # >>> g = corpus.parse('luca/gloria') 1507 # >>> gl = layout.divideByPages(g) 1508 # >>> gl.getAllMeasurePositionsInDocument() 1509 ''' 1510 numStaves = len(self.pages[0].systems[0].staves) 1511 allRetInfo = [] 1512 for mNum in range(self.measureStart, self.measureEnd + 1): 1513 if printUpdates is True: # so fast now that it's not needed 1514 print('Doing measure ', mNum) 1515 mList = [] 1516 for staffNum in range(numStaves): 1517 tupleInfo = self.getPositionForStaffMeasure(staffNum, mNum, returnFormat) 1518 infoDict = { 1519 'measureNumberActual': mNum, 1520 'measureNumber': mNum - 1, 1521 'staffNumber': staffNum, 1522 'top': tupleInfo[0][0], 1523 'left': tupleInfo[0][1], 1524 'bottom': tupleInfo[1][0], 1525 'right': tupleInfo[1][1], 1526 'pageNumber': tupleInfo[2], 1527 } 1528 mList.append(infoDict) 1529 allRetInfo.append(mList) 1530 return allRetInfo 1531 1532 1533class Page(stream.Opus): 1534 ''' 1535 Designation that all the music in this Stream 1536 belongs on a single notated page. 1537 ''' 1538 1539 def __init__(self, *args, **keywords): 1540 super().__init__(*args, **keywords) 1541 self.pageNumber = 1 1542 self.measureStart = None 1543 self.measureEnd = None 1544 self.systemStart = None 1545 self.systemEnd = None 1546 self.pageLayout = None 1547 1548 def _reprInternal(self): 1549 return f'p.{self.pageNumber}' 1550 1551 @property 1552 def systems(self): 1553 return self.getElementsByClass(System) 1554 1555 def show(self, fmt=None, app=None, **keywords): 1556 ''' 1557 Borrows stream.Score.show 1558 1559 >>> ls = layout.System() 1560 >>> lp = layout.Page() 1561 >>> lp.append(ls) 1562 >>> lp.show('text') 1563 {0.0} <music21.layout.System 0: p.0, sys.0> 1564 <BLANKLINE> 1565 ''' 1566 return stream.Score.show(self, fmt=fmt, app=app, **keywords) 1567 1568 1569 1570class System(stream.Score): 1571 ''' 1572 Designation that all the music in this Stream 1573 belongs on a single notated system. 1574 ''' 1575 1576 def __init__(self, *args, **keywords): 1577 super().__init__(*args, **keywords) 1578 self.systemNumber = 0 1579 1580 self.pageNumber = 0 1581 self.pageSystemNumber = 0 1582 1583 self.systemLayout = None 1584 self.measureStart = None 1585 self.measureEnd = None 1586 1587 def _reprInternal(self): 1588 return f'{self.systemNumber}: p.{self.pageNumber}, sys.{self.pageSystemNumber}' 1589 1590 @property 1591 def staves(self): 1592 return self.getElementsByClass(Staff) 1593 1594 1595class Staff(stream.Part): 1596 ''' 1597 Designation that all the music in this Stream 1598 belongs on a single Staff. 1599 ''' 1600 1601 def __init__(self, *args, **keywords): 1602 super().__init__(*args, **keywords) 1603 self.staffNumber = 1 # number in this system NOT GLOBAL 1604 1605 self.scoreStaffNumber = 0 1606 self.pageNumber = 0 1607 self.pageSystemNumber = 0 1608 1609 self.optimized = 0 1610 self.height = None # None = undefined 1611 self.inheritedHeight = None 1612 self.staffLayout = None 1613 1614 def _reprInternal(self): 1615 return '{0}: p.{1}, sys.{2}, st.{3}'.format(self.scoreStaffNumber, 1616 self.pageNumber, 1617 self.pageSystemNumber, 1618 self.staffNumber) 1619 1620 1621_DOC_ORDER = [ScoreLayout, PageLayout, SystemLayout, StaffLayout, LayoutBase, 1622 LayoutScore, Page, System, Staff] 1623# ------------------------------------------------------------------------------ 1624 1625 1626class Test(unittest.TestCase): 1627 1628 def testBasic(self): 1629 from music21 import note 1630 from music21.musicxml import m21ToXml 1631 s = stream.Stream() 1632 1633 for i in range(1, 11): 1634 m = stream.Measure() 1635 m.number = i 1636 n = note.Note() 1637 m.append(n) 1638 s.append(m) 1639 1640 sl = SystemLayout() 1641 # sl.isNew = True # this should not be on first system 1642 # as this causes all subsequent margins to be distorted 1643 sl.leftMargin = 300 1644 sl.rightMargin = 300 1645 s.getElementsByClass('Measure')[0].insert(0, sl) 1646 1647 sl = SystemLayout() 1648 sl.isNew = True 1649 sl.leftMargin = 200 1650 sl.rightMargin = 200 1651 sl.distance = 40 1652 s.getElementsByClass('Measure')[2].insert(0, sl) 1653 1654 sl = SystemLayout() 1655 sl.isNew = True 1656 sl.leftMargin = 220 1657 s.getElementsByClass('Measure')[4].insert(0, sl) 1658 1659 sl = SystemLayout() 1660 sl.isNew = True 1661 sl.leftMargin = 60 1662 sl.rightMargin = 300 1663 sl.distance = 200 1664 s.getElementsByClass('Measure')[6].insert(0, sl) 1665 1666 sl = SystemLayout() 1667 sl.isNew = True 1668 sl.leftMargin = 0 1669 sl.rightMargin = 0 1670 s.getElementsByClass('Measure')[8].insert(0, sl) 1671 1672 # systemLayoutList = s[music21.layout.SystemLayout] 1673 # self.assertEqual(len(systemLayoutList), 4) 1674 1675 # s.show() 1676 unused_raw = m21ToXml.GeneralObjectExporter().parse(s) 1677 1678 def x_testGetPageMeasureNumbers(self): 1679 from music21 import corpus 1680 c = corpus.parse('luca/gloria').parts[0] 1681 # c.show('text') 1682 retStr = '' 1683 for x in c.flatten(): 1684 if 'PageLayout' in x.classes: 1685 retStr += str(x.pageNumber) + ': ' + str(x.measureNumber) + ', ' 1686# print(retStr) 1687 self.assertEqual(retStr, '1: 1, 2: 23, 3: 50, 4: 80, 5: 103, ') 1688 1689 def testGetStaffLayoutFromStaff(self): 1690 ''' 1691 we have had problems with attributes disappearing. 1692 ''' 1693 from music21 import corpus 1694 lt = corpus.parse('demos/layoutTest.xml') 1695 ls = divideByPages(lt, fastMeasures=True) 1696 1697 hiddenStaff = ls.pages[0].systems[3].staves[1] 1698 self.assertTrue(repr(hiddenStaff).endswith('Staff 11: p.1, sys.4, st.2>'), 1699 repr(hiddenStaff)) 1700 self.assertIsNotNone(hiddenStaff.staffLayout) 1701 1702 1703# ------------------------------------------------------------------------------ 1704if __name__ == '__main__': 1705 import music21 1706 music21.mainTest(Test) # , runTest='getStaffLayoutFromStaff') 1707 1708 1709