-
Notifications
You must be signed in to change notification settings - Fork 402
/
base.py
14635 lines (12478 loc) · 553 KB
/
base.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
594
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: stream/base.py
# Purpose: base classes for dealing with groups of positioned objects
#
# Authors: Michael Scott Asato Cuthbert
# Christopher Ariza
# Josiah Wolf Oberholtzer
# Evan Lynch
#
# Copyright: Copyright © 2008-2023 Michael Scott Asato Cuthbert
# License: BSD, see license.txt
# -----------------------------------------------------------------------------
'''
The :class:`~music21.stream.Stream` and its subclasses
(which are themselves subclasses of the :class:`~music21.base.Music21Object`)
are the fundamental containers of offset-positioned notation and
musical elements in music21. Common Stream subclasses, such
as the :class:`~music21.stream.Measure`, :class:`~music21.stream.Part`
and :class:`~music21.stream.Score` objects, are also in this module.
'''
from __future__ import annotations
from collections import deque, namedtuple, OrderedDict
from collections.abc import Collection, Iterable, Sequence
import copy
from fractions import Fraction
import itertools
import math
from math import isclose
import os
import pathlib
import types
import typing as t
from typing import overload # pycharm bug disallows alias
import unittest
import warnings
from music21 import base
from music21 import bar
from music21 import common
from music21.common.enums import GatherSpanners, OffsetSpecial
from music21.common.numberTools import opFrac
from music21.common.types import (
StreamType, M21ObjType, ChangedM21ObjType, OffsetQL, OffsetQLSpecial
)
from music21 import clef
from music21 import chord
from music21 import defaults
from music21 import derivation
from music21 import duration
from music21 import environment
from music21 import exceptions21
from music21 import interval
from music21 import instrument
from music21 import key
from music21 import metadata
from music21 import meter
from music21 import note
from music21 import pitch
from music21 import tie
from music21 import repeat
from music21 import sites
from music21 import style
from music21 import tempo
from music21.stream import core
from music21.stream import makeNotation
from music21.stream import streamStatus
from music21.stream import iterator
from music21.stream import filters
from music21.stream.enums import GivenElementsBehavior, RecursionType, ShowNumber
if t.TYPE_CHECKING:
from music21 import spanner
environLocal = environment.Environment('stream')
StreamException = exceptions21.StreamException
ImmutableStreamException = exceptions21.ImmutableStreamException
T = t.TypeVar('T')
RecursiveLyricList = note.Lyric|None|list['RecursiveLyricList']
BestQuantizationMatch = namedtuple(
'BestQuantizationMatch',
['remainingGap', 'error', 'tick', 'match', 'signedError', 'divisor']
)
class StreamDeprecationWarning(UserWarning):
# Do not subclass Deprecation warning, because these
# warnings need to be passed to users...
pass
# -----------------------------------------------------------------------------
# Metaclass
OffsetMap = namedtuple('OffsetMap', ['element', 'offset', 'endTime', 'voiceIndex'])
# -----------------------------------------------------------------------------
class Stream(core.StreamCore, t.Generic[M21ObjType]):
'''
This is the fundamental container for Music21Objects;
objects may be ordered and/or placed in time based on
offsets from the start of this container.
As a subclass of Music21Object, Streams have offsets,
priority, id, and groups.
Streams may be embedded within other Streams. As each
Stream can have its own offset, when Streams are
embedded the offset of an element is relatively only
to its parent Stream. The :meth:`~music21.stream.Stream.flatten`
and method provides access to a flat version of all
embedded Streams, with offsets relative to the
top-level Stream.
The Stream :attr:`~music21.stream.Stream.elements` attribute
returns the contents of the Stream as a list. Direct access
to, and manipulation of, the elements list is not recommended.
Instead, use the host of high-level methods available.
The Stream, like all Music21Objects, has a
:class:`music21.duration.Duration` that is usually the
"release" time of the chronologically last element in the Stream
(that is, the highest onset plus the duration of
any element in the Stream).
The duration, however, can be "unlinked" and explicitly
set independent of the Stream's contents.
The first element passed to the Stream is an optional single
Music21Object or a list, tuple, or other Stream of Music21Objects
which is used to populate the Stream by inserting each object at
its :attr:`~music21.base.Music21Object.offset`
property. One special case is when every such object, such as a newly created
one, has no offset. Then, so long as the entire list is not composed of
non-Measure Stream subclasses representing synchrony like Parts or Voices,
each element is appended, creating a sequence of elements in time,
rather than synchrony.
Other arguments and keywords are ignored, but are
allowed so that subclassing the Stream is easier.
>>> s1 = stream.Stream()
>>> s1.append(note.Note('C#4', type='half'))
>>> s1.append(note.Note('D5', type='quarter'))
>>> s1.duration.quarterLength
3.0
>>> for thisNote in s1.notes:
... print(thisNote.octave)
...
4
5
This is a demonstration of creating a Stream with other elements,
including embedded Streams (in this case, :class:`music21.stream.Part`,
a Stream subclass):
>>> c1 = clef.TrebleClef()
>>> c1.offset = 0.0
>>> c1.priority = -1
>>> n1 = note.Note('E-6', type='eighth')
>>> n1.offset = 1.0
>>> p1 = stream.Part()
>>> p1.offset = 0.0
>>> p1.id = 'embeddedPart'
>>> p1.append(note.Rest()) # quarter rest
>>> s2 = stream.Stream([c1, n1, p1])
>>> s2.duration.quarterLength
1.5
>>> s2.show('text')
{0.0} <music21.clef.TrebleClef>
{0.0} <music21.stream.Part embeddedPart>
{0.0} <music21.note.Rest quarter>
{1.0} <music21.note.Note E->
* New in v7: providing a single element now works:
>>> s = stream.Stream(meter.TimeSignature())
>>> s.first()
<music21.meter.TimeSignature 4/4>
Providing a list of objects or Measures or Scores (but not other Stream
subclasses such as Parts or Voices) positions sequentially, i.e. appends, if they
all have offset 0.0 currently:
>>> s2 = stream.Measure([note.Note(), note.Note(), bar.Barline()])
>>> s2.show('text')
{0.0} <music21.note.Note C>
{1.0} <music21.note.Note C>
{2.0} <music21.bar.Barline type=regular>
A list of measures will thus each be appended:
>>> m1 = stream.Measure(n1, number=1)
>>> m2 = stream.Measure(note.Rest(), number=2)
>>> s3 = stream.Part([m1, m2])
>>> s3.show('text')
{0.0} <music21.stream.Measure 1 offset=0.0>
{1.0} <music21.note.Note E->
{1.5} <music21.stream.Measure 2 offset=1.5>
{0.0} <music21.note.Rest quarter>
Here, every element is a Stream that's not a Measure (or Score), so it
will be inserted at 0.0, rather than appending:
>>> s4 = stream.Score([stream.PartStaff(n1), stream.PartStaff(note.Rest())])
>>> s4.show('text')
{0.0} <music21.stream.PartStaff 0x...>
{1.0} <music21.note.Note E->
{0.0} <music21.stream.PartStaff 0x...>
{0.0} <music21.note.Rest quarter>
Create nested streams in one fell swoop:
>>> s5 = stream.Score(stream.Part(stream.Measure(chord.Chord('C2 A2'))))
>>> s5.show('text')
{0.0} <music21.stream.Part 0x...>
{0.0} <music21.stream.Measure 0 offset=0.0>
{0.0} <music21.chord.Chord C2 A2>
This behavior can be modified by the `givenElementsBehavior` keyword to go against the norm
of 'OFFSETS':
>>> from music21.stream.enums import GivenElementsBehavior
>>> s6 = stream.Stream([note.Note('C'), note.Note('D')],
... givenElementsBehavior=GivenElementsBehavior.INSERT)
>>> s6.show('text') # all notes at offset 0.0
{0.0} <music21.note.Note C>
{0.0} <music21.note.Note D>
>>> p1 = stream.Part(stream.Measure(note.Note('C')), id='p1')
>>> p2 = stream.Part(stream.Measure(note.Note('D')), id='p2')
>>> s7 = stream.Score([p1, p2],
... givenElementsBehavior=GivenElementsBehavior.APPEND)
>>> s7.show('text') # parts following each other (not recommended)
{0.0} <music21.stream.Part p1>
{0.0} <music21.stream.Measure 0 offset=0.0>
{0.0} <music21.note.Note C>
{1.0} <music21.stream.Part p2>
{0.0} <music21.stream.Measure 0 offset=0.0>
{0.0} <music21.note.Note D>
For developers of subclasses, please note that because of how Streams
are copied, there cannot be
required parameters (i.e., without defaults) in initialization.
For instance, this would not
be allowed, because craziness and givenElements are required::
class CrazyStream(Stream):
def __init__(self, givenElements, craziness, **keywords):
...
* New in v7: smart appending
* New in v8: givenElementsBehavior keyword configures the smart appending.
'''
# this static attributes offer a performance boost over other
# forms of checking class
isStream = True
isMeasure = False
classSortOrder: int|float = -20
recursionType: RecursionType = RecursionType.ELEMENTS_FIRST
_styleClass = style.StreamStyle
# define order of presenting names in documentation; use strings
_DOC_ORDER = ['append', 'insert', 'storeAtEnd', 'insertAndShift',
'recurse', 'flat',
'notes', 'pitches',
'transpose',
'augmentOrDiminish', 'scaleOffsets', 'scaleDurations']
# documentation for all attributes (not properties or methods)
_DOC_ATTR: dict[str, str] = {
'recursionType': '''
Class variable:
RecursionType Enum of (ELEMENTS_FIRST (default), FLATTEN, ELEMENTS_ONLY)
that decides whether the stream likely holds relevant
contexts for the elements in it.
Define this for a stream class, not an individual object.
see :meth:`~music21.base.Music21Object.contextSites`
''',
'isSorted': '''
Boolean describing whether the Stream is sorted or not.
''',
'autoSort': '''
Boolean describing whether the Stream is automatically sorted by
offset whenever necessary.
''',
'isFlat': '''
Boolean describing whether this Stream contains embedded
sub-Streams or Stream subclasses (not flat).
''',
'definesExplicitSystemBreaks': '''
Boolean that says whether all system breaks in the piece are
explicitly defined. Only used on musicxml output (maps to the
musicxml <supports attribute="new-system"> tag) and only if this is
the outermost Stream being shown
''',
'definesExplicitPageBreaks': '''
Boolean that says whether all page breaks in the piece are
explicitly defined. Only used on musicxml output (maps to the
musicxml <supports attribute="new-page"> tag) and only if this is
the outermost Stream being shown.
''',
# 'restrictClass': '''
# All elements in the stream are required to be of this class
# or a subclass of that class. Currently not enforced. Used
# for type-checking.
# ''',
}
def __init__(self,
givenElements: t.Union[None,
base.Music21Object,
Sequence[base.Music21Object]] = None,
*,
givenElementsBehavior: GivenElementsBehavior = GivenElementsBehavior.OFFSETS,
**keywords):
# restrictClass: type[M21ObjType] = base.Music21Object,
super().__init__(**keywords)
# TEMPORARY variable for v9 to deprecate the flat property. -- remove in v10
self._created_via_deprecated_flat = False
self.streamStatus = streamStatus.StreamStatus(self)
self._unlinkedDuration = None
self.autoSort = True
# # all elements in the stream need to be of this class.
# self.restrictClass = restrictClass
# these should become part of style or something else...
self.definesExplicitSystemBreaks = False
self.definesExplicitPageBreaks = False
# property for transposition status;
self._atSoundingPitch: bool|t.Literal['unknown'] = 'unknown'
# experimental
self._mutable = True
if givenElements is None:
return
if isinstance(givenElements, base.Music21Object):
givenElements = t.cast(list[base.Music21Object], [givenElements])
# Append rather than insert if every offset is 0.0
# but not if every element is a stream subclass other than a Measure or Score
# (i.e. Part or Voice generally, but even Opus theoretically)
# because these classes usually represent synchrony
appendBool = True
if givenElementsBehavior == GivenElementsBehavior.OFFSETS:
try:
appendBool = all(e.offset == 0.0 for e in givenElements)
except AttributeError:
pass # appropriate failure will be raised by coreGuardBeforeAddElement()
if appendBool and all(
(e.isStream and e.classSet.isdisjoint((Measure, Score)))
for e in givenElements):
appendBool = False
elif givenElementsBehavior == GivenElementsBehavior.INSERT:
appendBool = False
else:
appendBool = True
for e in givenElements:
self.coreGuardBeforeAddElement(e)
if appendBool:
self.coreAppend(e)
else:
self.coreInsert(e.offset, e)
self.coreElementsChanged()
def __eq__(self, other):
'''
No two streams are ever equal unless they are the same Stream
'''
return self is other
def __hash__(self) -> int:
return id(self) >> 4
def _reprInternal(self) -> str:
if self.id is not None:
if self.id != id(self) and str(self.id) != str(id(self)):
return str(self.id)
elif isinstance(self.id, int):
return hex(self.id)
else: # pragma: no cover
return ''
else: # pragma: no cover
return ''
def write(self, fmt=None, fp=None, **keywords):
# ... --- see base.py calls .write(
if self.isSorted is False and self.autoSort: # pragma: no cover
self.sort()
return super().write(fmt=fmt, fp=fp, **keywords)
def show(self, fmt=None, app=None, **keywords):
# ... --- see base.py calls .write(
if self.isSorted is False and self.autoSort:
self.sort()
return super().show(fmt=fmt, app=app, **keywords)
# --------------------------------------------------------------------------
# sequence like operations
def __len__(self) -> int:
'''
Get the total number of elements in the Stream.
This method does not recurse into and count elements in contained Streams.
>>> import copy
>>> a = stream.Stream()
>>> for x in range(4):
... n = note.Note('G#')
... n.offset = x * 3
... a.insert(n)
>>> len(a)
4
>>> b = stream.Stream()
>>> for x in range(4):
... b.insert(copy.deepcopy(a)) # append streams
>>> len(b)
4
>>> len(b.flatten())
16
Includes end elements:
>>> b.storeAtEnd(bar.Barline('double'))
>>> len(b)
5
'''
return len(self._elements) + len(self._endElements)
def __iter__(self) -> iterator.StreamIterator[M21ObjType]:
'''
The Stream iterator, used in all for
loops and similar iteration routines. This method returns the
specialized :class:`music21.stream.StreamIterator` class, which
adds necessary Stream-specific features.
'''
# temporary for v9 -- remove in v10
if self._created_via_deprecated_flat:
warnings.warn('.flat is deprecated. Call .flatten() instead',
exceptions21.Music21DeprecationWarning,
stacklevel=3)
self._created_via_deprecated_flat = False
return t.cast(iterator.StreamIterator[M21ObjType],
iterator.StreamIterator(self))
def iter(self) -> iterator.StreamIterator[M21ObjType]:
'''
The Stream iterator, used in all for
loops and similar iteration routines. This method returns the
specialized :class:`music21.stream.StreamIterator` class, which
adds necessary Stream-specific features.
Generally you don't need this, just iterate over a stream, but it is necessary
to add custom filters to an iterative search before iterating.
'''
# Pycharm wasn't inferring typing correctly with `return iter(self)`.
return self.__iter__() # pylint: disable=unnecessary-dunder-call
@overload
def __getitem__(self, k: str) -> iterator.RecursiveIterator[M21ObjType]:
# Remove this code and replace with ... once Astroid #1015 is fixed.
x: iterator.RecursiveIterator[M21ObjType] = self.recurse()
return x
@overload
def __getitem__(self, k: int) -> M21ObjType:
return self[k] # dummy code
@overload
def __getitem__(self, k: slice) -> list[M21ObjType]:
return list(self.elements) # dummy code
@overload
def __getitem__(
self,
k: type[ChangedM21ObjType]
) -> iterator.RecursiveIterator[ChangedM21ObjType]:
x = t.cast(iterator.RecursiveIterator[ChangedM21ObjType], self.recurse())
return x # dummy code
@overload
def __getitem__(
self,
k: type # getting something that is a subclass of something that is not a m21 object
) -> iterator.RecursiveIterator[M21ObjType]:
x = t.cast(iterator.RecursiveIterator[M21ObjType], self.recurse())
return x # dummy code
@overload
def __getitem__(
self,
k: Collection[type]
) -> iterator.RecursiveIterator[M21ObjType]:
# Remove this code and replace with ... once Astroid #1015 is fixed.
x: iterator.RecursiveIterator[M21ObjType] = self.recurse()
return x
def __getitem__(self,
k: t.Union[str,
int,
slice,
type[ChangedM21ObjType],
Collection[type]]
) -> t.Union[iterator.RecursiveIterator[M21ObjType],
iterator.RecursiveIterator[ChangedM21ObjType],
M21ObjType,
list[M21ObjType]]:
'''
Get a Music21Object from the Stream using a variety of keys or indices.
If an int is given, the Music21Object at the index is returned, as if it were a list
or tuple:
>>> c = note.Note('C')
>>> d = note.Note('D')
>>> e = note.Note('E')
>>> r1 = note.Rest()
>>> f = note.Note('F')
>>> g = note.Note('G')
>>> r2 = note.Rest()
>>> a = note.Note('A')
>>> s = stream.Stream([c, d, e, r1, f, g, r2, a])
>>> s[0]
<music21.note.Note C>
>>> s[-1]
<music21.note.Note A>
Out of range notes raise an IndexError:
>>> s[99]
Traceback (most recent call last):
IndexError: attempting to access index 99 while elements is of size 8
If a slice of indices is given, a list of elements is returned, as if the Stream
were a list or Tuple.
>>> subslice = s[2:5]
>>> subslice
[<music21.note.Note E>, <music21.note.Rest quarter>, <music21.note.Note F>]
>>> len(subslice)
3
>>> s[1].offset
1.0
>>> subslice[1].offset
3.0
If a class is given, then a :class:`~music21.stream.iterator.RecursiveIterator`
of elements matching the requested class is returned, similar
to `Stream().recurse().getElementsByClass()`.
>>> len(s)
8
>>> len(s[note.Rest])
2
>>> len(s[note.Note])
6
>>> for n in s[note.Note]:
... print(n.name, end=' ')
C D E F G A
Note that this iterator is recursive: it will find elements inside of streams
within this stream:
>>> c_sharp = note.Note('C#')
>>> v = stream.Voice()
>>> v.insert(0, c_sharp)
>>> s.insert(0.5, v)
>>> len(s[note.Note])
7
When using a single Music21 class in this way, your type checker will
be able to infer that the only objects in any loop are in fact `note.Note`
objects, and catch programming errors before running.
Multiple classes can be provided, separated by commas. Any element matching
any of the requested classes will be matched.
>>> len(s[note.Note, note.Rest])
9
>>> for note_or_rest in s[note.Note, note.Rest]:
... if isinstance(note_or_rest, note.Note):
... print(note_or_rest.name, end=' ')
... else:
... print('Rest', end=' ')
C C# D E Rest F G Rest A
The actual object returned by `s[module.Class]` is a
:class:`~music21.stream.iterator.RecursiveIterator` and has all the functions
available on it:
>>> s[note.Note]
<...>
If no elements of the class are found, no error is raised in version 7:
>>> list(s[layout.StaffLayout])
[]
If the key is a string, it is treated as a `querySelector` as defined in
:meth:`~music21.stream.iterator.getElementsByQuerySelector`, namely that bare strings
are treated as class names, strings beginning with `#` are id-queries, and strings
beginning with `.` are group queries.
We can set some ids and groups for demonstrating.
>>> a.id = 'last_a'
>>> c.groups.append('ghost')
>>> e.groups.append('ghost')
'.ghost', because it begins with `.`, is treated as a class name and
returns a `RecursiveIterator`:
>>> for n in s['.ghost']:
... print(n.name, end=' ')
C E
A query selector with a `#` returns the single element matching that
element or returns None if there is no match:
>>> s['#last_a']
<music21.note.Note A>
>>> s['#nothing'] is None
True
Any other query raises a TypeError:
>>> s[0.5]
Traceback (most recent call last):
TypeError: Streams can get items by int, slice, class, class iterable, or string query;
got <class 'float'>
* Changed in v7:
- out of range indexes now raise an IndexError, not StreamException
- strings ('music21.note.Note', '#id', '.group') are now treated like a query selector.
- slices with negative indices now supported
- Unsupported types now raise TypeError
- Class and Group searches now return a recursive `StreamIterator` rather than a `Stream`
- Slice searches now return a list of elements rather than a `Stream`
* Changed in v8:
- for strings: only fully-qualified names such as "music21.note.Note" or
partially-qualified names such as "note.Note" are
supported as class names. Better to use a literal type or explicitly call
.recurse().getElementsByClass to get the earlier behavior. Old behavior
still works until v9. This is an attempt to unify __getitem__ behavior in
StreamIterators and Streams.
- allowed iterables of qualified class names, e.g. `[note.Note, note.Rest]`
'''
# need to sort if not sorted, as this call may rely on index positions
if not self.isSorted and self.autoSort:
self.sort() # will set isSorted to True
if isinstance(k, int):
match = None
# handle easy and most common case first
if 0 <= k < len(self._elements):
match = self._elements[k]
# if using negative indices, or trying to access end elements,
# then need to use .elements property
else:
try:
match = self.elements[k]
except IndexError:
raise IndexError(
f'attempting to access index {k} '
+ f'while elements is of size {len(self.elements)}'
)
# setting active site as cautionary measure
self.coreSelfActiveSite(match)
return t.cast(M21ObjType, match)
elif isinstance(k, slice): # get a slice of index values
# manually inserting elements is critical to setting the element
# locations
searchElements: list[base.Music21Object] = self._elements
if (k.start is not None and k.start < 0) or (k.stop is not None and k.stop < 0):
# Must use .elements property to incorporate end elements
searchElements = list(self.elements)
return t.cast(M21ObjType, searchElements[k])
elif isinstance(k, type):
if issubclass(k, base.Music21Object):
return self.recurse().getElementsByClass(k)
else:
# this is explicitly NOT true, but we're pretending
# it is a Music21Object for now, because the only things returnable
# from getElementsByClass are Music21Objects that also inherit from k.
m21Type = t.cast(type[M21ObjType], k) # type: ignore
return self.recurse().getElementsByClass(m21Type)
elif common.isIterable(k) and all(isinstance(maybe_type, type) for maybe_type in k):
return self.recurse().getElementsByClass(k)
elif isinstance(k, str):
querySelectorIterator = self.recurse().getElementsByQuerySelector(k)
if '#' in k:
# an id anywhere in the query selector should return only one element
return querySelectorIterator.first()
else:
return querySelectorIterator
raise TypeError(
'Streams can get items by int, slice, class, class iterable, or string query; '
f'got {type(k)}'
)
def first(self) -> M21ObjType|None:
'''
Return the first element of a Stream. (Added for compatibility with StreamIterator)
Or None if the Stream is empty.
Unlike s.iter().first(), which is a significant performance gain, s.first() is the
same speed as s[0], except for not raising an IndexError.
>>> nC = note.Note('C4')
>>> nD = note.Note('D4')
>>> s = stream.Stream()
>>> s.append([nC, nD])
>>> s.first()
<music21.note.Note C>
>>> empty = stream.Stream()
>>> print(empty.first())
None
* New in v7.
'''
try:
return self[0]
except IndexError:
return None
def last(self) -> M21ObjType|None:
'''
Return the last element of a Stream. (Added for compatibility with StreamIterator)
Or None if the Stream is empty.
s.last() is the same speed as s[-1], except for not raising an IndexError.
>>> nC = note.Note('C4')
>>> nD = note.Note('D4')
>>> s = stream.Stream()
>>> s.append([nC, nD])
>>> s.last()
<music21.note.Note D>
>>> empty = stream.Stream()
>>> print(empty.last())
None
* New in v7.
'''
try:
return self[-1]
except IndexError:
return None
def __contains__(self, el: base.Music21Object) -> bool:
'''
Returns True if `el` is in the stream (compared with Identity) and False otherwise.
>>> nC = note.Note('C4')
>>> nD = note.Note('D4')
>>> s = stream.Stream()
>>> s.append(nC)
>>> nC in s
True
>>> nD in s
False
Note that we match on actual `id()` equality (`x is y`) and not on
`==` equality.
>>> nC2 = note.Note('C4')
>>> nC == nC2
True
>>> nC2 in s
False
To get the latter, compare on `.elements` which uses Python's
default `__contains__` for tuples.
>>> nC2 in s.elements
True
'''
# Should be the fastest implementation of this naive check, compare with
# https://stackoverflow.com/questions/44802682/python-any-unexpected-performance
return (id(el) in self._offsetDict
or any(True for sEl in self._elements if sEl is el)
or any(True for sEl in self._endElements if sEl is el))
@property
def elements(self) -> tuple[M21ObjType, ...]:
'''
.elements is a Tuple representing the elements contained in the Stream.
Directly getting, setting, and manipulating this Tuple is
reserved for advanced usage. Instead, use the
provided high-level methods. The elements retrieved here may not
have this stream as an activeSite, therefore they might not be properly ordered.
In other words: Don't use unless you really know what you're doing.
Treat a Stream like a list!
See how these are equivalent:
>>> m = stream.Measure([note.Note('F4'), note.Note('G4')])
>>> m.elements
(<music21.note.Note F>, <music21.note.Note G>)
>>> tuple(m)
(<music21.note.Note F>, <music21.note.Note G>)
When setting .elements, a list of Music21Objects can be provided, or a complete Stream.
If a complete Stream is provided, elements are extracted
from that Stream. This has the advantage of transferring
offset correctly and getting elements stored at the end.
>>> a = stream.Stream()
>>> a.repeatInsert(note.Note('C'), list(range(10)))
>>> b = stream.Stream()
>>> b.repeatInsert(note.Note('D'), list(range(10)))
>>> b.offset = 6
>>> c = stream.Stream()
>>> c.repeatInsert(note.Note('E'), list(range(10)))
>>> c.offset = 12
>>> b.insert(c)
>>> b.isFlat
False
>>> a.isFlat
True
Assigning from a Stream works well, and is actually much safer than assigning
from `.elements` of the other Stream, since the active sites may have changed
of that stream's elements in the meantime.
>>> a.elements = b
>>> a.isFlat
False
>>> len(a.recurse().notes) == len(b.recurse().notes) == 20
True
There is one good use for .elements as opposed to treating a Stream like a list,
and that is that `in` for Streams compares on object identity, i.e.,
id(a) == id(b) [this is for historical reasons], while since `.elements`
is a tuple. Recall our measure with the notes F4 and G4 above.
>>> other_g = note.Note('G4')
This new G can't be found in m, because it is not physically in the Measure
>>> other_g in m
False
But it is *equal* to something in the Measure:
>>> other_g in m.elements
True
But again, this could be done simply with:
>>> other_g in tuple(m)
True
One reason to use `.elements` is to iterate quickly without setting
activeSite:
>>> n = note.Note()
>>> m1 = stream.Measure([n])
>>> m2 = stream.Measure([n])
>>> n.activeSite is m2
True
>>> for el in m1.elements:
... pass
>>> n.activeSite is m2
True
>>> for el in m1:
... pass
>>> n.activeSite is m1
True
'''
# combines _elements and _endElements into one.
if 'elements' not in self._cache or self._cache['elements'] is None:
# this list concatenation may take time; thus, only do when
# coreElementsChanged has been called
if not self.isSorted and self.autoSort:
self.sort() # will set isSorted to True
self._cache['elements'] = t.cast(list[M21ObjType], self._elements + self._endElements)
return tuple(self._cache['elements'])
@elements.setter
def elements(self, value: Stream|Iterable[base.Music21Object]):
'''
Sets this stream's elements to the elements in another stream (just give
the stream, not the stream's .elements), or to a list of elements.
Safe:
newStream.elements = oldStream
Unsafe:
newStream.elements = oldStream.elements
Why?
The activeSites of some elements may have changed between retrieving
and setting (esp. if a lot else has happened in the meantime). Where
are we going to get the new stream's elements' offsets from? why
from their active sites! So don't do this!
'''
self._offsetDict: dict[int, tuple[OffsetQLSpecial, base.Music21Object]] = {}
if isinstance(value, Stream):
# set from a Stream. Best way to do it
self._elements: list[base.Music21Object] = list(value._elements) # copy list.
for e in self._elements:
self.coreSetElementOffset(e, value.elementOffset(e), addElement=True)
e.sites.add(self)
self.coreSelfActiveSite(e)
self._endElements: list[base.Music21Object] = list(value._endElements)
for e in self._endElements:
self.coreSetElementOffset(e,
value.elementOffset(e, returnSpecial=True),
addElement=True)
e.sites.add(self)
self.coreSelfActiveSite(e)
else:
# replace the complete elements list
self._elements = list(value)
self._endElements = []
for e in self._elements:
self.coreSetElementOffset(e, e.offset, addElement=True)
e.sites.add(self)
self.coreSelfActiveSite(e)
self.coreElementsChanged()
def __setitem__(self, k, value):
'''
Insert an item at a currently filled index position,
as represented in the elements list.
>>> a = stream.Stream()
>>> a.repeatInsert(note.Note('C'), list(range(10)))
>>> b = stream.Stream()
>>> b.repeatInsert(note.Note('C'), list(range(10)))
>>> b.offset = 6
>>> c = stream.Stream()
>>> c.repeatInsert(note.Note('C'), list(range(10)))
>>> c.offset = 12
>>> b.insert(c)
>>> a.isFlat
True
>>> a[3] = b
>>> a.isFlat
False
'''
# remove old value at this position
oldValue = self._elements[k]
del self._offsetDict[id(oldValue)]
oldValue.sites.remove(self)
oldValue.activeSite = None
# assign in new position
self._elements[k] = value
self.coreSetElementOffset(value, value.offset, addElement=True)
self.coreSelfActiveSite(value)
# must get native offset
value.sites.add(self)
if isinstance(value, Stream):
# know that this is now not flat