-
Notifications
You must be signed in to change notification settings - Fork 402
/
layout.py
1687 lines (1383 loc) · 62.5 KB
/
layout.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
863
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# -*- coding: utf-8 -*-
# ------------------------------------------------------------------------------
# Name: layout.py
# Purpose: Layout objects
#
# Authors: Christopher Ariza
# Michael Scott Asato Cuthbert
#
# Copyright: Copyright © 2010, 2012 Michael Scott Asato Cuthbert
# License: BSD, see license.txt
# ------------------------------------------------------------------------------
'''
The layout.py module contains two types of objects that specify the layout on
page (or screen) for Scores and other Stream objects. There are two main types
of Layout objects: (1) layout describing elements and (2) layout defining Streams.
(1) ScoreLayout, PageLayout, SystemLayout, and StaffLayout objects describe the size of
pages, the geometry of page and system margins, the distance between staves, etc.
The model for these layout objects is taken directly (perhaps too directly?)
from MusicXML. These objects all inherit from a BaseLayout class, primarily
as an aid to finding all of these objects as a group. ScoreLayouts give defaults
for each page, system, and staff. Thus, they contain PageLayout, SystemLayout, and
currently one or more StaffLayout objects (but probably just one. MusicXML allows more than
StaffLayout object because multiple staves can be in a Part. Music21 uses
the concept of a PartStaff for a Part that is played by the same performer as another.
e.g., the left hand of the Piano is a PartStaff paired with the right hand).
PageLayout and SystemLayout objects also have a property, 'isNew',
which, if set to `True`, signifies that a new page
or system should begin here. In theory, one could define new dimensions for a page
or system in the middle of the system or page without setting isNew to True, in
which case these measurements would start applying on the next page. In practice,
there's really one good place to use these Layout objects and that's in the first part
in a score at offset 0 of the first measure on a page or system
(or for ScoreLayout, at the beginning
of a piece outside any parts). But it's not an
error to put them in other places, such as at offset 0 of the first measure of a page
or system in all the other parts. In fact, MusicXML tends to do this, and it ends up
not being a waste if a program extracts a single part from the middle of a score.
These objects are standard :class:`~music21.base.Music21Object` types, but many
properties such as .duration, .beat, will probably not apply.
When exporting to MusicXML (which is currently the only format in which music21 can and
does preserve these markings), many MusicXML readers will ignore these tags (or worse,
add a new page or system when PageLayout and SystemLayout objects are found but also
add theme wherever they want). In Finale, this behavior disappears if the MusicXML
document notes that it <supports> new-page and new-system markings. Music21 will add
the appropriate <supports> tags if the containing Stream has `.definesExplicitPageBreaks`
and `.definesExplicitSystemBreaks` set to True. When importing a score that has the
<supports> tag set, music21 will set `.definesExplicitXXXXBreaks` to True for the
outer score and the inner parts. However, this means that if the score is manipulated
enough that the prior layout information is obsolete, programs will need to set these
properties to False or move the Layout tags accordingly.
(2) The second set of objects are Stream subclasses that can be employed when a program
needs to easily iterate around the systems and pages defined through the layout objects
just described, or to get the exact position on a page (or a graphical representation
of a page) for a particular measure or system. (Individual notes coming soon). Normal
Score streams can be changed into LayoutStreams by calling `divideByPages(s)` on them.
A Score that was organized: Score->Part->Measure would then become:
LayoutScore->Page->System->Staff->Measure.
The new LayoutScore has methods that enable querying what page or system a measure is in, and
specifically where on a page a measure is (or the dimensions
of every measure in the piece). However
do not call .show() on a LayoutScore -- the normal score it's derived from will work just fine.
Nor does calling .show() on a Page or System work yet, but once the LayoutStream has been created,
code like this can be done:
s = stream.Stream(...)
ls = layout.divideByPages(s)
pg2sys3 = ls.pages[1].systems[2] # n.b.! 1, 2
measureStart, measureEnd = pg2sys3.measureStart, pg2sys3.measureEnd
scoreExcerpt = s.measures(measureStart, measureEnd)
scoreExcerpt.show() # will show page 2, system 3
Note that while the coordinates given by music21 for a musicxml score (based on margins,
staff size, etc.)
generally reflect what is actually in a musicxml producer, unfortunately, x-positions are
far less accurately
produced by most editors. For instance, Finale scores with measure sizes that have been
manually adjusted tend to show their
unadjusted measure width and not their actual measure width in the MusicXML.
SmartScore Pro tends to produce very good MusicXML layout data.
'''
from __future__ import annotations
# may need to have an object to convert between size units
import copy
import unittest
from collections import namedtuple
import typing as t
from music21 import base
from music21.common.enums import GatherSpanners
from music21 import environment
from music21 import exceptions21
from music21 import spanner
from music21 import stream
from music21.stream.enums import StaffType
environLocal = environment.Environment('layout')
SystemSize = namedtuple('SystemSize', ['top', 'left', 'right', 'bottom'])
PageSize = namedtuple('PageSize', ['top', 'left', 'right', 'bottom', 'width', 'height'])
class LayoutBase(base.Music21Object):
'''
A base class for all Layout objects, defining a classSortOrder
and also an inheritance tree.
>>> scoreLayout = layout.ScoreLayout()
>>> isinstance(scoreLayout, layout.LayoutBase)
True
'''
classSortOrder = -10
def _reprInternal(self):
return ''
# ------------------------------------------------------------------------------
class ScoreLayout(LayoutBase):
'''
Parameters for configuring a score's layout.
PageLayout objects may be found on Measure or Part Streams.
>>> pl = layout.PageLayout(pageNumber=4, leftMargin=234, rightMargin=124,
... pageHeight=4000, pageWidth=3000, isNew=True)
>>> pl.pageNumber
4
>>> pl.rightMargin
124
>>> pl.leftMargin
234
>>> pl.isNew
True
This object represents both <print new-page> and <page-layout>
elements in musicxml. The appearance tag is handled in the `.style`
for the stream (it was here in v7 and before, but did nothing).
Note that the appearance and style elements are subject to change during
and after the v8 releases.
'''
# TODO -- make sure that the first pageLayout and systemLayout
# for each page are working together.
def __init__(self,
*,
scalingMillimeters: int | float | None = None,
scalingTenths: int | float | None = None,
musicFont: str | None = None,
wordFont: str | None = None,
pageLayout: PageLayout | None = None,
systemLayout: SystemLayout | None = None,
staffLayoutList: list[StaffLayout] | None = None,
**keywords):
super().__init__(**keywords)
self.scalingMillimeters = scalingMillimeters
self.scalingTenths = scalingTenths
self.pageLayout: PageLayout | None = pageLayout
self.systemLayout: SystemLayout | None = systemLayout
self.staffLayoutList: list[StaffLayout] = []
self.musicFont = musicFont
self.wordFont = wordFont
if staffLayoutList is not None:
self.staffLayoutList = staffLayoutList
def tenthsToMillimeters(self, tenths):
'''
given the scalingMillimeters and scalingTenths,
return the value in millimeters of a number of
musicxml "tenths" where a tenth is a tenth of the distance
from one staff line to another
returns 0.0 if either of scalingMillimeters or scalingTenths
is undefined.
>>> sl = layout.ScoreLayout(scalingMillimeters=2.0, scalingTenths=10)
>>> print(sl.tenthsToMillimeters(10))
2.0
>>> print(sl.tenthsToMillimeters(17)) # printing to round
3.4
'''
if self.scalingMillimeters is None or self.scalingTenths is None:
return 0.0
millimetersPerTenth = float(self.scalingMillimeters) / self.scalingTenths
return round(millimetersPerTenth * tenths, 6)
# ------------------------------------------------------------------------------
class PageLayout(LayoutBase):
'''
Parameters for configuring a page's layout.
PageLayout objects may be found on Measure or Part Streams.
>>> pl = layout.PageLayout(pageNumber=4, leftMargin=234, rightMargin=124,
... pageHeight=4000, pageWidth=3000, isNew=True)
>>> pl.pageNumber
4
>>> pl.rightMargin
124
>>> pl.leftMargin
234
>>> pl.isNew
True
This object represents both <print new-page> and <page-layout>
elements in musicxml.
'''
# TODO -- make sure that the first pageLayout and systemLayout
# for each page are working together.
def __init__(self,
*,
pageNumber: int | None = None,
leftMargin: int | float | None = None,
rightMargin: int | float | None = None,
topMargin: int | float | None = None,
bottomMargin: int | float | None = None,
pageHeight: int | float | None = None,
pageWidth: int | float | None = None,
isNew: bool | None = None,
**keywords):
super().__init__(**keywords)
self.pageNumber = pageNumber
self.leftMargin = leftMargin
self.rightMargin = rightMargin
self.topMargin = topMargin
self.bottomMargin = bottomMargin
self.pageHeight = pageHeight
self.pageWidth = pageWidth
# store if this is the start of a new page
self.isNew = isNew
# ------------------------------------------------------------------------------
class SystemLayout(LayoutBase):
'''
Object that configures or alters a system's layout.
SystemLayout objects may be found on Measure or
Part Streams.
Importantly, if isNew is True then this object
indicates that a new system should start here.
>>> sl = layout.SystemLayout(leftMargin=234, rightMargin=124, distance=3, isNew=True)
>>> sl.distance
3
>>> sl.rightMargin
124
>>> sl.leftMargin
234
>>> sl.isNew
True
'''
def __init__(self,
*,
leftMargin: int | float | None = None,
rightMargin: int | float | None = None,
distance: int | float | None = None,
topDistance: int | float | None = None,
isNew: bool | None = None,
**keywords):
super().__init__(**keywords)
self.leftMargin = leftMargin
self.rightMargin = rightMargin
# no top or bottom margins
# this is probably the distance between adjacent systems
self.distance = distance
self.topDistance = topDistance
# store if this is the start of a new system
self.isNew = isNew
class StaffLayout(LayoutBase):
'''
Object that configures or alters the distance between
one staff and another in a system.
StaffLayout objects may be found on Measure or
Part Streams.
The musicxml equivalent <staff-layout> lives in
the <defaults> and in <print> attributes.
>>> sl = layout.StaffLayout(distance=3, staffNumber=1, staffSize=113, staffLines=5)
>>> sl.distance
3
The "number" attribute refers to which staff number
in a part group this refers to. Thus, it's not
necessary in music21, but we store it if it's there.
(defaults to None)
>>> sl.staffNumber
1
staffLines specifies the number of lines for a non 5-line staff.
>>> sl.staffLines
5
staffSize is a percentage of the base staff size, so
this defines a staff 13% larger than normal. Note that it is always converted to
a floating point number.
>>> sl.staffSize
113.0
>>> sl
<music21.layout.StaffLayout distance 3, staffNumber 1, staffSize 113.0, staffLines 5>
StaffLayout can also specify the staffType:
>>> sl.staffType = stream.enums.StaffType.OSSIA
There is one other attribute, '.hidden' which has three settings:
* None - inherit from previous StaffLayout object, or False if no object exists
* False - not hidden -- show as a default staff
* True - hidden -- for playback only staves, or for a hidden/optimized-out staff
Note: (TODO: .hidden None is not working; always gives False)
'''
_DOC_ATTR: dict[str, str] = {
'staffType': '''
What kind of staff is this as a stream.enums.StaffType.
>>> sl = layout.StaffLayout()
>>> sl.staffType
<StaffType.REGULAR: 'regular'>
>>> sl.staffType = stream.enums.StaffType.CUE
>>> sl.staffType
<StaffType.CUE: 'cue'>
''',
}
def __init__(self,
*,
distance: int | float | None = None,
staffNumber: int | float | None = None,
staffSize: int | float | None = None,
staffLines: int | None = None,
hidden: bool | None = None,
staffType: StaffType = StaffType.REGULAR,
**keywords):
super().__init__(**keywords)
# this is the distance between adjacent staves
self.distance = distance
self.staffNumber = staffNumber
self.staffSize: float | None = None if staffSize is None else float(staffSize)
self.staffLines = staffLines
self.hidden = hidden # True = hidden; False = shown; None = inherit
self.staffType: StaffType = staffType
def _reprInternal(self):
return (f'distance {self.distance!r}, staffNumber {self.staffNumber!r}, '
f'staffSize {self.staffSize!r}, staffLines {self.staffLines!r}')
# ------------------------------------------------------------------------------
class LayoutException(exceptions21.Music21Exception):
pass
class StaffGroupException(spanner.SpannerException):
pass
# ------------------------------------------------------------------------------
class StaffGroup(spanner.Spanner):
'''
A StaffGroup defines a collection of one or more
:class:`~music21.stream.Part` objects,
specifying that they should be shown together with a bracket,
brace, or other symbol, and may have a common name.
>>> p1 = stream.Part()
>>> p2 = stream.Part()
>>> p1.append(note.Note('C5', type='whole'))
>>> p1.append(note.Note('D5', type='whole'))
>>> p2.append(note.Note('C3', type='whole'))
>>> p2.append(note.Note('D3', type='whole'))
>>> p3 = stream.Part()
>>> p3.append(note.Note('F#4', type='whole'))
>>> p3.append(note.Note('G#4', type='whole'))
>>> s = stream.Score()
>>> s.insert(0, p1)
>>> s.insert(0, p2)
>>> s.insert(0, p3)
>>> staffGroup1 = layout.StaffGroup([p1, p2],
... name='Marimba', abbreviation='Mba.', symbol='brace')
>>> staffGroup1.barTogether = 'Mensurstrich'
>>> s.insert(0, staffGroup1)
>>> staffGroup2 = layout.StaffGroup([p3],
... name='Xylophone', abbreviation='Xyl.', symbol='bracket')
>>> s.insert(0, staffGroup2)
>>> #_DOCS_SHOW s.show()
.. image:: images/layout_StaffGroup_01.*
:width: 400
'''
def __init__(self,
*spannedElements,
name: str | None = None,
barTogether: t.Literal[True, False, None, 'Mensurstrich'] = True,
abbreviation: str | None = None,
symbol: t.Literal['bracket', 'line', 'brace', 'square'] | None = None,
**keywords):
super().__init__(*spannedElements, **keywords)
self.name = name or abbreviation # if this group has a name
self.abbreviation = abbreviation
self._symbol: t.Literal['bracket', 'line', 'brace', 'square'] | None = None
self.symbol = symbol
# determines if barlines are grouped through; this is group barline
# in musicxml
self._barTogether = barTogether
# --------------------------------------------------------------------------
def _getBarTogether(self) -> t.Literal[True, False, None, 'Mensurstrich']:
return self._barTogether
def _setBarTogether(self, value: t.Literal[True, False, None, 'Mensurstrich', 'yes', 'no']):
if value is None:
pass # do nothing for now; could set a default
elif value in ['yes', True]:
self._barTogether = True
elif value in ['no', False]:
self._barTogether = False
elif isinstance(value, str) and value.lower() == 'mensurstrich':
self._barTogether = 'Mensurstrich'
else:
raise StaffGroupException(f'the bar together value {value} is not acceptable')
barTogether = property(_getBarTogether, _setBarTogether, doc='''
Get or set the barTogether value, with either Boolean values
or yes or no strings. Or the string 'Mensurstrich' which
indicates barring between staves but not in staves.
Currently Mensurstrich is not supported by most exporters.
>>> sg = layout.StaffGroup()
>>> sg.barTogether = 'yes'
>>> sg.barTogether
True
>>> sg.barTogether = 'Mensurstrich'
>>> sg.barTogether
'Mensurstrich'
''')
def _getSymbol(self) -> t.Literal['bracket', 'line', 'brace', 'square'] | None:
return self._symbol
def _setSymbol(self, value: t.Literal['bracket', 'line', 'brace', 'square'] | None):
if value is None or str(value).lower() == 'none':
self._symbol = None
elif value.lower() in ['brace', 'line', 'bracket', 'square']:
self._symbol = t.cast(t.Literal['bracket', 'line', 'brace', 'square'], value.lower())
else:
raise StaffGroupException(f'the symbol value {value} is not acceptable')
symbol = property(_getSymbol, _setSymbol, doc='''
Get or set the symbol value, with either Boolean values or yes or no strings.
>>> sg = layout.StaffGroup()
>>> sg.symbol = 'Brace'
>>> sg.symbol
'brace'
''')
# ---------------------------------------------------------------
# Stream subclasses for layout
def divideByPages(
scoreIn: stream.Score,
printUpdates: bool = False,
fastMeasures: bool = False
) -> LayoutScore:
'''
Divides a score into a series of smaller scores according to page
breaks. Only searches for PageLayout.isNew or SystemLayout.isNew
on the first part. Returns a new `LayoutScore` object.
If fastMeasures is True, then the newly created System objects
do not have Clef signs, Key Signatures, or necessarily all the
applicable spanners in them. On the other hand, the position
(on the page) information will be just as correct with
fastMeasures = True and it will run much faster on large scores
(because our spanner gathering algorithm is currently O(n^2);
something TODO: to fix.)
>>> lt = corpus.parse('demos/layoutTest.xml')
>>> len(lt.parts)
3
>>> len(lt.parts[0].getElementsByClass(stream.Measure))
80
Divide the score up into layout.Page objects
>>> layoutScore = layout.divideByPages(lt, fastMeasures=True)
>>> len(layoutScore.pages)
4
>>> lastPage = layoutScore.pages[-1]
>>> lastPage.measureStart
64
>>> lastPage.measureEnd
80
the layoutScore is a subclass of stream.Opus:
>>> layoutScore
<music21.layout.LayoutScore ...>
>>> 'Opus' in layoutScore.classes
True
Pages are subclasses of Opus also, since they contain Scores
>>> lastPage
<music21.layout.Page ...>
>>> 'Opus' in lastPage.classes
True
Each page now has Systems not parts.
>>> firstPage = layoutScore.pages[0]
>>> len(firstPage.systems)
4
>>> firstSystem = firstPage.systems[0]
>>> firstSystem.measureStart
1
>>> firstSystem.measureEnd
5
Systems are a subclass of Score:
>>> firstSystem
<music21.layout.System ...>
>>> isinstance(firstSystem, stream.Score)
True
Each System has staves (layout.Staff objects) not parts, though Staff is a subclass of Part
>>> secondStaff = firstSystem.staves[1]
>>> print(len(secondStaff.getElementsByClass(stream.Measure)))
5
>>> secondStaff
<music21.layout.Staff ...>
>>> isinstance(secondStaff, stream.Part)
True
'''
def getRichSystemLayout(inner_allSystemLayouts):
'''
If there are multiple systemLayouts in an iterable (list or StreamIterator),
make a copy of the first one and get information from each successive one into
a rich system layout.
'''
richestSystemLayout = copy.deepcopy(inner_allSystemLayouts[0])
for sl in inner_allSystemLayouts[1:]:
for attribute in ('distance', 'topDistance', 'leftMargin', 'rightMargin'):
if (getattr(richestSystemLayout, attribute) is None
and getattr(sl, attribute) is not None):
setattr(richestSystemLayout, attribute, getattr(sl, attribute))
return richestSystemLayout
pageMeasureTuples = getPageRegionMeasureNumbers(scoreIn)
systemMeasureTuples = getSystemRegionMeasureNumbers(scoreIn)
firstMeasureNumber = pageMeasureTuples[0][0]
lastMeasureNumber = pageMeasureTuples[-1][1]
scoreLists = LayoutScore()
scoreLists.definesExplicitPageBreaks = True
scoreLists.definesExplicitSystemBreaks = True
scoreLists.measureStart = firstMeasureNumber
scoreLists.measureEnd = lastMeasureNumber
for el in scoreIn:
if not isinstance(el, stream.Part):
if 'ScoreLayout' in el.classes:
scoreLists.scoreLayout = el
scoreLists.insert(scoreIn.elementOffset(el), el)
pageNumber = 0
systemNumber = 0
scoreStaffNumber = 0
for pageStartM, pageEndM in pageMeasureTuples:
pageNumber = 1
if printUpdates is True:
print('updating page', pageNumber)
thisPage = Page()
thisPage.measureStart = pageStartM
thisPage.measureEnd = pageEndM
thisPage.pageNumber = pageNumber
if fastMeasures is True:
thisPageAll = scoreIn.measures(pageStartM, pageEndM,
collect=[],
gatherSpanners=GatherSpanners.NONE)
else:
thisPageAll = scoreIn.measures(pageStartM, pageEndM)
thisPage.systemStart = systemNumber 1
for el in thisPageAll:
if not isinstance(el.classes and 'StaffGroup' not in el, stream.Part):
thisPage.insert(thisPageAll.elementOffset(el), el)
firstMeasureOfFirstPart = thisPageAll.parts.first().getElementsByClass(
stream.Measure).first()
for el in firstMeasureOfFirstPart:
if 'PageLayout' in el.classes:
thisPage.pageLayout = el
pageSystemNumber = 0
for systemStartM, systemEndM in systemMeasureTuples:
if systemStartM < pageStartM or systemEndM > pageEndM:
continue
systemNumber = 1 # global, not on this page...
pageSystemNumber = 1
if fastMeasures is True:
measureStacks = scoreIn.measures(systemStartM, systemEndM,
collect=[],
gatherSpanners=GatherSpanners.NONE)
else:
measureStacks = scoreIn.measures(systemStartM, systemEndM)
thisSystem = System()
thisSystem.systemNumber = systemNumber
thisSystem.pageNumber = pageNumber
thisSystem.pageSystemNumber = pageSystemNumber
thisSystem.mergeAttributes(measureStacks)
thisSystem.elements = measureStacks
thisSystem.measureStart = systemStartM
thisSystem.measureEnd = systemEndM
systemStaffNumber = 0
for p in list(thisSystem.parts):
scoreStaffNumber = 1
systemStaffNumber = 1
staffObject = Staff()
staffObject.mergeAttributes(p)
staffObject.scoreStaffNumber = scoreStaffNumber
staffObject.staffNumber = systemStaffNumber
staffObject.pageNumber = pageNumber
staffObject.pageSystemNumber = pageSystemNumber
# until getters/setters can have different types
staffObject.elements = p # type: ignore
thisSystem.replace(p, staffObject)
allStaffLayouts: list[StaffLayout] = list(p[StaffLayout])
if not allStaffLayouts:
continue
# else:
staffObject.staffLayout = allStaffLayouts[0]
# if len(allStaffLayouts) > 1:
# print('Got many staffLayouts')
allSystemLayouts = thisSystem[SystemLayout]
if len(allSystemLayouts) >= 2:
thisSystem.systemLayout = getRichSystemLayout(list(allSystemLayouts))
elif len(allSystemLayouts) == 1:
thisSystem.systemLayout = allSystemLayouts[0]
else:
thisSystem.systemLayout = None
thisPage.coreAppend(thisSystem)
thisPage.systemEnd = systemNumber
thisPage.coreElementsChanged()
scoreLists.coreAppend(thisPage)
scoreLists.coreElementsChanged()
return scoreLists
def getPageRegionMeasureNumbers(scoreIn):
return getRegionMeasureNumbers(scoreIn, 'Page')
def getSystemRegionMeasureNumbers(scoreIn):
return getRegionMeasureNumbers(scoreIn, 'System')
def getRegionMeasureNumbers(scoreIn, region='Page'):
'''
get a list where each entry is a 2-tuplet whose first number
refers to the first measure on a page and whose second number
is the last measure on the page.
'''
if region == 'Page':
classesToReturn = ['PageLayout']
elif region == 'System':
classesToReturn = ['PageLayout', 'SystemLayout']
else:
raise ValueError('region must be one of Page or System')
firstPart = scoreIn.parts.first()
# first measure could be 1 or 0 (or something else)
allMeasures = firstPart.getElementsByClass(stream.Measure)
firstMeasureNumber = allMeasures.first().number
lastMeasureNumber = allMeasures.last().number
measureStartList = [firstMeasureNumber]
measureEndList = []
allAppropriateLayout = firstPart.flatten().getElementsByClass(classesToReturn)
for pl in allAppropriateLayout:
plMeasureNumber = pl.measureNumber
if pl.isNew is False:
continue
if plMeasureNumber not in measureStartList:
# in case of firstMeasureNumber or system and page layout at same time.
measureStartList.append(plMeasureNumber)
measureEndList.append(plMeasureNumber - 1)
measureEndList.append(lastMeasureNumber)
measureList = list(zip(measureStartList, measureEndList))
return measureList
class LayoutScore(stream.Opus):
'''
Designation that this Score is
divided into Pages, Systems, Staves (=Parts),
Measures, etc.
Used for computing location of notes, etc.
If the score does not change between calls to the various getPosition calls,
it is much faster as it uses a cache.
'''
def __init__(self, givenElements=None, **keywords):
super().__init__(givenElements, **keywords)
self.scoreLayout = None
self.measureStart = None
self.measureEnd = None
@property
def pages(self):
return self.getElementsByClass(Page)
def show(self, fmt=None, app=None, **keywords):
'''
Borrows stream.Score.show
>>> lp = layout.Page()
>>> ls = layout.LayoutScore()
>>> ls.append(lp)
>>> ls.show('text')
{0.0} <music21.layout.Page p.1>
<BLANKLINE>
'''
return stream.Score.show(self, fmt=fmt, app=app, **keywords)
def getPageAndSystemNumberFromMeasureNumber(self, measureNumber):
'''
Given a layoutScore from divideByPages and a measureNumber returns a tuple
of (pageId, systemId). Note that pageId is probably one less than the page number,
assuming that the first page number is 1, the pageId for the first page will be 0.
Similarly, the first systemId on each page will be 0
>>> lt = corpus.parse('demos/layoutTest.xml')
>>> l = layout.divideByPages(lt, fastMeasures=True)
>>> l.getPageAndSystemNumberFromMeasureNumber(80)
(3, 3)
'''
if 'pageAndSystemNumberFromMeasureNumbers' not in self._cache:
self._cache['pageAndSystemNumberFromMeasureNumbers'] = {}
dataCache = self._cache['pageAndSystemNumberFromMeasureNumbers']
if measureNumber in dataCache:
return dataCache[measureNumber]
foundPage = None
foundPageId = None
for pageId, thisPage in enumerate(self.pages):
if measureNumber < thisPage.measureStart or measureNumber > thisPage.measureEnd:
continue
foundPage = thisPage
foundPageId = pageId
break
if foundPage is None:
raise LayoutException('Cannot find this measure on any page!')
foundSystem = None
foundSystemId = None
for systemId, thisSystem in enumerate(foundPage.systems):
if measureNumber < thisSystem.measureStart or measureNumber > thisSystem.measureEnd:
continue
foundSystem = thisSystem
foundSystemId = systemId
break
if foundSystem is None:
raise LayoutException("that's strange, this measure was supposed to be on this page, "
"but I couldn't find it anywhere!")
dataCache[measureNumber] = (foundPageId, foundSystemId)
return (foundPageId, foundSystemId)
def getMarginsAndSizeForPageId(self, pageId):
'''
return a namedtuple of (top, left, bottom, right, width, height)
margins for a given pageId in tenths
Default of (100, 100, 100, 100, 850, 1100) if undefined
>>> #_DOCS_SHOW g = corpus.parse('luca/gloria')
>>> #_DOCS_SHOW m22 = g.parts[0].getElementsByClass(stream.Measure)[22]
>>> #_DOCS_SHOW m22.getElementsByClass(layout.PageLayout).first().leftMargin = 204.0
>>> #_DOCS_SHOW gl = layout.divideByPages(g)
>>> #_DOCS_SHOW gl.getMarginsAndSizeForPageId(1)
>>> layout.PageSize(171.0, 204.0, 171.0, 171.0, 1457.0, 1886.0) #_DOCS_HIDE
PageSize(top=171.0, left=204.0, right=171.0, bottom=171.0, width=1457.0, height=1886.0)
'''
if 'marginsAndSizeForPageId' not in self._cache:
self._cache['marginsAndSizeForPageId'] = {}
dataCache = self._cache['marginsAndSizeForPageId']
if pageId in dataCache:
return dataCache[pageId]
# define defaults
pageMarginTop = 100
pageMarginLeft = 100
pageMarginRight = 100
pageMarginBottom = 100
pageWidth = 850
pageHeight = 1100
thisPage = self.pages[pageId]
# override defaults with scoreLayout
if self.scoreLayout is not None:
scl = self.scoreLayout
if scl.pageLayout is not None:
pl = scl.pageLayout
if pl.pageWidth is not None:
pageWidth = pl.pageWidth
if pl.pageHeight is not None:
pageHeight = pl.pageHeight
if pl.topMargin is not None:
pageMarginTop = pl.topMargin
if pl.leftMargin is not None:
pageMarginLeft = pl.leftMargin
if pl.rightMargin is not None:
pageMarginRight = pl.rightMargin
if pl.bottomMargin is not None:
pageMarginBottom = pl.bottomMargin
# override global information with page specific pageLayout
if thisPage.pageLayout is not None:
pl = thisPage.pageLayout
if pl.pageWidth is not None:
pageWidth = pl.pageWidth
if pl.pageHeight is not None:
pageHeight = pl.pageHeight
if pl.topMargin is not None:
pageMarginTop = pl.topMargin
if pl.leftMargin is not None:
pageMarginLeft = pl.leftMargin
if pl.rightMargin is not None:
pageMarginRight = pl.rightMargin
if pl.bottomMargin is not None:
pageMarginBottom = pl.bottomMargin
dataTuple = PageSize(pageMarginTop, pageMarginLeft, pageMarginBottom, pageMarginRight,
pageWidth, pageHeight)
dataCache[pageId] = dataTuple
return dataTuple
def getPositionForSystem(self, pageId, systemId):
'''
first systems on a page use a different positioning.
returns a Named tuple of the (top, left, right, and bottom) where each unit is
relative to the page margins
N.B. right is NOT the width -- it is different. It is the offset to the right margin.
weird, inconsistent, but most useful...bottom is the hard part to compute...
>>> lt = corpus.parse('demos/layoutTestMore.xml')
>>> ls = layout.divideByPages(lt, fastMeasures = True)
>>> ls.getPositionForSystem(0, 0)
SystemSize(top=211.0, left=70.0, right=0.0, bottom=696.0)
>>> ls.getPositionForSystem(0, 1)
SystemSize(top=810.0, left=0.0, right=0.0, bottom=1173.0)
>>> ls.getPositionForSystem(0, 2)
SystemSize(top=1340.0, left=67.0, right=92.0, bottom=1610.0)
>>> ls.getPositionForSystem(0, 3)
SystemSize(top=1724.0, left=0.0, right=0.0, bottom=2030.0)
>>> ls.getPositionForSystem(0, 4)
SystemSize(top=2144.0, left=0.0, right=0.0, bottom=2583.0)
'''
if 'positionForSystem' not in self._cache:
self._cache['positionForSystem'] = {}
positionForSystemCache = self._cache['positionForSystem']
cacheKey = f'{pageId}-{systemId}'
if cacheKey in positionForSystemCache:
return positionForSystemCache[cacheKey]
if pageId == 0 and systemId == 4:
pass
leftMargin = 0
rightMargin = 0
# no top or bottom margins
# distance from previous
previousDistance = 0
# override defaults with scoreLayout
if self.scoreLayout is not None:
scl = self.scoreLayout
if scl.systemLayout is not None:
sl = scl.systemLayout
if sl.leftMargin is not None:
leftMargin = sl.leftMargin
if sl.rightMargin is not None:
rightMargin = sl.rightMargin
if systemId == 0:
if sl.topDistance is not None:
previousDistance = sl.topDistance
else:
if sl.distance is not None:
previousDistance = sl.distance
# override global information with system specific pageLayout
thisSystem = self.pages[pageId].systems[systemId]
if thisSystem.systemLayout is not None:
sl = thisSystem.systemLayout
if sl.leftMargin is not None:
leftMargin = sl.leftMargin
if sl.rightMargin is not None:
rightMargin = sl.rightMargin
if systemId == 0:
if sl.topDistance is not None:
previousDistance = sl.topDistance
else:
if sl.distance is not None:
previousDistance = sl.distance
if systemId > 0:
lastSystemDimensions = self.getPositionForSystem(pageId, systemId - 1)
bottomOfLastSystem = lastSystemDimensions.bottom
else:
bottomOfLastSystem = 0
numStaves = len(thisSystem.staves)
lastStaff = numStaves - 1 #
unused_systemStart, systemHeight = self.getPositionForStaff(pageId, systemId, lastStaff)
top = previousDistance bottomOfLastSystem
bottom = top systemHeight
dataTuple = SystemSize(float(top), float(leftMargin), float(rightMargin), float(bottom))
positionForSystemCache[cacheKey] = dataTuple
return dataTuple
def getPositionForStaff(self, pageId, systemId, staffId):
'''
return a tuple of (top, bottom) for a staff, specified by a given pageId,
systemId, and staffId in tenths of a staff-space.
This distance is specified with respect to the top of the system.
Staff scaling (<staff-details> in musicxml inside an <attributes> object) is
taken into account, but not non-five-line staves. Thus, a normally sized staff
is always of height 40 (4 spaces of 10-tenths each)
>>> lt = corpus.parse('demos/layoutTest.xml')
>>> ls = layout.divideByPages(lt, fastMeasures=True)