1# -*- coding: utf-8 -*-
2# ------------------------------------------------------------------------------
3# Name:         segment.py
4# Purpose:      Division of stream.Part into segments for individual handling
5# Authors:      Jose Cabal-Ugaz
6#
7# Copyright:    Copyright © 2012 Michael Scott Cuthbert and the music21 Project
8# License:      BSD, see license.txt
9# ------------------------------------------------------------------------------
10'''
11Inner classes and functions for transcribing musical segments into braille.
12
13This module was made in consultation with the manual "Introduction to Braille
14Music Transcription, Second Edition" by Mary Turner De Garmo, 2005. It is
15available from the Library of Congress
16`here <https://www.loc.gov/nls/braille-audio-reading-materials/music-materials/>`_,
17and will henceforth be referred to as BMTM.
18'''
19import collections
20import copy
21import enum
22import unittest
23
24from typing import Optional
25
26from music21 import bar
27from music21 import chord
28from music21 import clef
29from music21 import dynamics
30from music21 import exceptions21
31from music21 import environment
32from music21 import expressions
33from music21 import key
34from music21 import layout
35from music21 import meter
36from music21 import note
37from music21 import spanner
38from music21 import stream
39from music21 import tempo
40from music21.prebase import ProtoM21Object
41
42from music21.braille import basic
43from music21.braille import lookup
44from music21.braille import noteGrouping as ngMod
45from music21.braille import text
46from music21.braille.objects import BrailleTranscriptionHelper
47
48from music21.common.numberTools import opFrac
49
50symbols = lookup.symbols
51environRules = environment.Environment('segment.py')
52
53
54# ------------------------------------------------------------------------------
55class BrailleSegmentException(exceptions21.Music21Exception):
56    pass
57
58
59class Affinity(enum.IntEnum):
60    _LOWEST = -1
61    SIGNATURE = 3
62    TTEXT = 4
63    MMARK = 5
64    LONG_TEXTEXPR = 6
65    INACCORD = 7
66
67    SPLIT1_NOTEGROUP = 8
68    NOTEGROUP = 9
69    SPLIT2_NOTEGROUP = 10
70
71
72# Class Sort Order -- differs for Braille than for general music21
73CSO_NOTE = 10
74CSO_REST = 10
75CSO_CHORD = 10
76CSO_DYNAMIC = 9
77CSO_CLEF = 7
78CSO_BARLINE = 0
79CSO_KEYSIG = 1
80CSO_TIMESIG = 2
81CSO_TTEXT = 3
82CSO_MMARK = 4
83CSO_VOICE = 10
84
85# (music21Object, affinity code, class sort order)
86affinityCodes = [(note.Note, Affinity.NOTEGROUP, CSO_NOTE),
87                 (note.Rest, Affinity.NOTEGROUP, CSO_REST),
88                 (chord.Chord, Affinity.NOTEGROUP, CSO_CHORD),
89                 (dynamics.Dynamic, Affinity.NOTEGROUP, CSO_DYNAMIC),
90                 (clef.Clef, Affinity.NOTEGROUP, CSO_CLEF),
91                 (bar.Barline, Affinity.SPLIT2_NOTEGROUP, CSO_BARLINE),
92                 (key.KeySignature, Affinity.SIGNATURE, CSO_KEYSIG),
93                 (meter.TimeSignature, Affinity.SIGNATURE, CSO_TIMESIG),
94                 (tempo.TempoText, Affinity.TTEXT, CSO_TTEXT),
95                 (tempo.MetronomeMark, Affinity.MMARK, CSO_MMARK),
96                 (stream.Voice, Affinity.INACCORD, CSO_VOICE)]
97
98affinityNames = {Affinity.SIGNATURE: 'Signature Grouping',
99                 Affinity.TTEXT: 'Tempo Text Grouping',
100                 Affinity.MMARK: 'Metronome Mark Grouping',
101                 Affinity.LONG_TEXTEXPR: 'Long Text Expression Grouping',
102                 Affinity.INACCORD: 'Inaccord Grouping',
103                 Affinity.NOTEGROUP: 'Note Grouping',
104                 Affinity.SPLIT1_NOTEGROUP: 'Split Note Grouping A',
105                 Affinity.SPLIT2_NOTEGROUP: 'Split Note Grouping B',
106                 }
107
108excludeFromBrailleElements = [spanner.Slur,
109                              layout.SystemLayout,
110                              layout.PageLayout,
111                              layout.StaffLayout]
112
113# Uncomment when Python 3.8 is the minimum version
114# from typing import TypedDict, Optional
115# class GroupingGlobals(TypedDict):
116#    keySignature: Optional[key.KeySignature]
117#    timeSignature: Optional[meter.TimeSignature]
118# GROUPING_GLOBALS: GroupingGlobals = {...}
119
120
121GROUPING_GLOBALS = {
122    'keySignature': None,  # will be key.KeySignature(0) on first call
123    'timeSignature': None,  # will be meter.TimeSignature('4/4') on first call
124}
125
126
127def setGroupingGlobals():
128    '''
129    sets defaults for grouping globals.  Called first time anything
130    in Braille is run, but saves creating two expensive objects if never run
131    '''
132    if GROUPING_GLOBALS['keySignature'] is None:
133        # remove noinspection when Python 3.8 is the minimum
134        # noinspection PyTypeChecker
135        GROUPING_GLOBALS['keySignature'] = key.KeySignature(0)
136    if GROUPING_GLOBALS['timeSignature'] is None:
137        # remove noinspection when Python 3.8 is the minimum
138        # noinspection PyTypeChecker
139        GROUPING_GLOBALS['timeSignature'] = meter.TimeSignature('4/4')
140
141
142SEGMENT_MAXNOTESFORSHORTSLUR = 4
143
144MAX_ELEMENTS_IN_SEGMENT = 48  # 8 measures of 6 notes, etc. each
145
146_ThreeDigitNumber = collections.namedtuple('_ThreeDigitNumber', 'hundreds tens ones')
147
148SegmentKey = collections.namedtuple('SegmentKey', 'measure ordinal affinity hand')
149SegmentKey.__new__.__defaults__ = (0, 0, None, None)
150
151
152# ------------------------------------------------------------------------------
153
154class BrailleElementGrouping(ProtoM21Object):
155    _DOC_ATTR = {
156        'keySignature': 'The last :class:`~music21.key.KeySignature` preceding the grouping.',
157        'timeSignature': 'The last :class:`~music21.meter.TimeSignature` preceding the grouping.',
158        'descendingChords': '''True if a :class:`~music21.chord.Chord` should be spelled
159             from highest to lowest pitch
160             in braille, False if the opposite is the case.''',
161        'showClefSigns': '''If True, clef signs are shown in braille.
162             Representation of music in braille is not
163             dependent upon clefs and staves, so the clef signs would be displayed
164             for referential or historical purposes.''',
165        #         'upperFirstInNoteFingering' : 'No documentation.',
166        'withHyphen': 'If True, this grouping will end with a music hyphen.',
167        'numRepeats': 'The number of times this grouping is repeated.'
168    }
169    def __init__(self, *args):
170        '''
171        A BrailleElementGrouping is a superclass of list of objects which should be displayed
172        without a space in braille.
173
174        >>> from music21.braille import segment
175        >>> bg = segment.BrailleElementGrouping()
176        >>> bg.append(note.Note('C4'))
177        >>> bg.append(note.Note('D4'))
178        >>> bg.append(note.Rest())
179        >>> bg.append(note.Note('F4'))
180        >>> bg
181        <music21.braille.segment.BrailleElementGrouping [<music21.note.Note C>,
182            <music21.note.Note D>, <music21.note.Rest quarter>, <music21.note.Note F>]>
183        >>> print(bg)
184        <music21.note.Note C>
185        <music21.note.Note D>
186        <music21.note.Rest quarter>
187        <music21.note.Note F>
188
189        These are the defaults and they are shared across all objects...
190
191        >>> bg.keySignature
192        <music21.key.KeySignature of no sharps or flats>
193        >>> bg.timeSignature
194        <music21.meter.TimeSignature 4/4>
195
196        >>> bg.descendingChords
197        True
198
199        >>> bg.showClefSigns
200        False
201
202        >>> bg.upperFirstInNoteFingering
203        True
204
205        >>> bg.withHyphen
206        False
207
208        >>> bg.numRepeats
209        0
210        '''
211        super().__init__()
212        self.internalList = list(*args)
213        setGroupingGlobals()
214
215        self.keySignature = GROUPING_GLOBALS['keySignature']
216        self.timeSignature = GROUPING_GLOBALS['timeSignature']
217        self.descendingChords = True
218        self.showClefSigns = False
219        self.upperFirstInNoteFingering = True
220        self.withHyphen = False
221        self.numRepeats = 0
222
223    def __getitem__(self, item):
224        return self.internalList[item]
225
226    def __setitem__(self, pos, item):
227        self.internalList[pos] = item
228
229    def __len__(self):
230        return len(self.internalList)
231
232    def __getattr__(self, attr):
233        if attr == 'internalList':
234            raise AttributeError('internalList not defined yet')
235        return getattr(self.internalList, attr)
236
237    def __str__(self):
238        '''
239        Return an unicode braille representation
240        of each object in the BrailleElementGrouping.
241        '''
242        allObjects = []
243        for obj in self:
244            if isinstance(obj, stream.Voice):
245                for obj2 in obj:
246                    try:
247                        allObjects.append('\n'.join(obj2.editorial.brailleEnglish))
248                    except (AttributeError, TypeError):
249                        allObjects.append(str(obj2))
250            else:
251                try:
252                    allObjects.append('\n'.join(obj.editorial.brailleEnglish))
253                except (AttributeError, TypeError):
254                    allObjects.append(str(obj))
255        if self.numRepeats > 0:
256            allObjects.append(f'** Grouping x {self.numRepeats + 1} **')
257        if self.withHyphen is True:
258            allObjects.append(f'music hyphen {lookup.symbols["music_hyphen"]}')
259        out = '\n'.join(allObjects)
260        return out
261
262    def _reprInternal(self):
263        return repr(self.internalList)
264
265
266class BrailleSegment(text.BrailleText):
267    _DOC_ATTR = {
268        'cancelOutgoingKeySig': '''If True, the previous key signature should be
269                 cancelled immediately before a new key signature is encountered.''',
270        'dummyRestLength': '''For a given positive integer n, adds n "dummy rests"
271                 near the beginning of a segment. Designed for test purposes, as they
272                 are used to demonstrate measure division at the end of braille lines.''',
273        'lineLength': '''The maximum amount of braille characters that should be
274                 present in a line. The standard is 40 characters.''',
275        'showFirstMeasureNumber': '''If True, then a measure number is shown
276                 following the heading (if applicable) and preceding the music.''',
277        'showHand': '''If set to "right" or "left", shows the corresponding
278                 hand sign at the beginning of the first line.''',
279        'showHeading': '''If True, then a braille heading is displayed.
280                 See :meth:`~music21.braille.basic.transcribeHeading`
281                 for more details on headings.''',
282        'suppressOctaveMarks': '''If True, then all octave marks are suppressed.
283                 Designed for test purposes, as octave marks were not presented
284                 until Chapter 7 of BMTM.''',
285        'endHyphen': '''If True, then the last
286                 :class:`~music21.braille.segment.BrailleElementGrouping` of this
287                 segment will be followed by a music hyphen.
288                 The last grouping is incomplete, because a segment
289                 break occurred in the middle of a measure.''',
290        'beginsMidMeasure': '''If True, then the initial measure number of this
291                 segment should be followed by a dot. This segment
292                 is starting in the middle of a measure.'''
293    }
294
295    def __init__(self, lineLength: int = 40):
296        '''
297        A segment is "a group of measures occupying more than one braille line."
298        Music is divided into segments so as to "present the music to the reader
299        in a meaningful manner and to give him convenient reference points to
300        use in memorization" (BMTM, 71).
301
302        >>> brailleSeg = braille.segment.BrailleSegment()
303
304        >>> brailleSeg.cancelOutgoingKeySig
305        True
306        >>> brailleSeg.dummyRestLength
307
308        >>> brailleSeg.lineLength
309        40
310
311        >>> brailleSeg.showFirstMeasureNumber
312        True
313
314
315        Possible showHand values are None, 'right', 'left':
316
317        >>> brailleSeg.showHand is None
318        True
319
320        >>> brailleSeg.showHeading
321        True
322
323        >>> brailleSeg.suppressOctaveMarks
324        False
325
326        >>> brailleSeg.endHyphen
327        False
328
329        >>> brailleSeg.beginsMidMeasure
330        False
331
332
333        A BrailleSegment is a type of defaultdict that returns a BrailleElementGrouping
334        when a key is missing.
335
336        >>> len(brailleSeg.keys())
337        0
338        >>> beg = brailleSeg[braille.segment.SegmentKey(4, 1, 9)]
339        >>> type(beg) is braille.segment.BrailleElementGrouping
340        True
341
342
343        Of course, creating random keys like this will have consequences:
344
345        >>> print(str(brailleSeg))
346        ---begin segment---
347        <music21.braille.segment BrailleSegment>
348        Measure 4, Note Grouping 2:
349        <BLANKLINE>
350        ===
351        ---end segment---
352        '''
353        super().__init__(lineLength=lineLength)
354        self._groupingDict = {}
355
356        self.groupingKeysToProcess = None
357        self.currentGroupingKey = None
358        self.lastNote = None
359        self.previousGroupingKey = None
360
361        self.showClefSigns: bool = False
362        self.upperFirstInNoteFingering: bool = True
363        self.descendingChords: bool = True
364
365        self.cancelOutgoingKeySig = True
366        self.dummyRestLength = None
367        self.showFirstMeasureNumber = True
368        self.showHand = None  # override with None, 'left', or 'right'
369        self.showHeading = True
370        self.suppressOctaveMarks = False
371        self.endHyphen = False
372        self.beginsMidMeasure = False
373
374    def __setitem__(self, item, value):
375        self._groupingDict[item] = value
376
377    def __getitem__(self, item):
378        if item not in self._groupingDict:
379            self._groupingDict[item] = BrailleElementGrouping()
380        return self._groupingDict[item]
381
382    def __delitem__(self, item):
383        if item not in self.__dict__:
384            del self._groupingDict[item]
385        else:
386            return ValueError(f'No item {item!r} in Segment')
387
388    def __getattr__(self, item):
389        return getattr(self._groupingDict, item)
390
391    def __contains__(self, item):
392        return item in self._groupingDict
393
394    def __iter__(self):
395        return iter(self._groupingDict)
396
397    def __len__(self):
398        return len(self._groupingDict)
399
400    @property
401    def brailleText(self):
402        '''
403        Returns the string from the BrailleText object
404        '''
405        return text.BrailleText.__str__(self)
406
407    def __str__(self):
408        name = '<music21.braille.segment BrailleSegment>'
409
410        allItems = sorted(self.items())
411        allKeys = []
412        allGroupings = []
413        # noinspection PyArgumentList
414        prevKey = SegmentKey()  # defaults are defined.
415
416        for (itemKey, grouping) in allItems:
417            try:
418                if prevKey.affinity == Affinity.SPLIT1_NOTEGROUP:
419                    prevKey = itemKey
420                    continue
421            except TypeError:
422                pass
423            allKeys.append('Measure {0}, {1} {2}:\n'.format(itemKey.measure,
424                                                             affinityNames[itemKey.affinity],
425                                                             itemKey.ordinal + 1))
426            gStr = str(grouping)
427            allGroupings.append(gStr)
428            prevKey = itemKey
429        allElementGroupings = '\n'.join([''.join([k, g, '\n==='])
430                                          for (k, g) in list(zip(allKeys, allGroupings))])
431        out = '\n'.join(['---begin segment---',
432                          name,
433                          allElementGroupings,
434                          '---end segment---'])
435        return out
436
437    def transcribe(self):
438        '''
439        transcribes all of the noteGroupings in this dict by:
440
441        first transcribing the Heading (if applicable)
442        then the Measure Number
443        then adds appropriate numbers of dummyRests
444        then adds the Rest of the Note Groupings
445
446        returns brailleText
447        '''
448        # noinspection PyAttributeOutsideInit
449        self.groupingKeysToProcess = list(sorted(self.keys()))
450
451        if self.showHeading:
452            self.extractHeading()  # Heading
453
454        if self.showFirstMeasureNumber:
455            self.extractMeasureNumber()  # Measure Number
456
457        if self.dummyRestLength is not None:
458            self.addDummyRests()  # Dummy Rests
459
460        self.previousGroupingKey = None
461        while self.groupingKeysToProcess:
462            # noinspection PyAttributeOutsideInit
463            self.currentGroupingKey = self.groupingKeysToProcess.pop(0)
464
465            cgkAffinityGroup = self.currentGroupingKey.affinity
466
467            if cgkAffinityGroup == Affinity.NOTEGROUP:
468                self.extractNoteGrouping()  # Note Grouping
469            elif cgkAffinityGroup == Affinity.SIGNATURE:
470                self.extractSignatureGrouping()  # Signature(s) Grouping
471            elif cgkAffinityGroup == Affinity.LONG_TEXTEXPR:
472                self.extractLongExpressionGrouping()  # Long Expression(s) Grouping
473            # elif cgkAffinityGroup == Affinity.INACCORD:
474            #     self.extractInaccordGrouping()  # In Accord Grouping
475            elif cgkAffinityGroup == Affinity.TTEXT:
476                self.extractTempoTextGrouping()  # Tempo Text Grouping
477            # noinspection PyAttributeOutsideInit
478            self.previousGroupingKey = self.currentGroupingKey
479
480        return self.brailleText
481
482    def addDummyRests(self):
483        '''
484        Adds as many dummy rests as self.dummyRestLength to the signatures of
485        brailleText
486
487        >>> seg = braille.segment.BrailleSegment()
488        >>> seg.dummyRestLength = 4
489
490        >>> print(braille.lookup.rests['dummy'])
491492        >>> seg.addDummyRests()
493        >>> print(seg.brailleText)
494        ⠄⠄⠄⠄
495        '''
496        dummyRests = [self.dummyRestLength * lookup.rests['dummy']]
497        self.addSignatures(''.join(dummyRests))
498
499    def extractMeasureNumber(self):
500        '''
501        Adds a measure number from the segmentKey needing processing
502
503        >>> segKey = braille.segment.SegmentKey(measure=4, ordinal=1, affinity=9)
504        >>> seg = braille.segment.BrailleSegment()
505
506        Initialize a new Key
507
508        >>> type(seg[segKey])
509        <class 'music21.braille.segment.BrailleElementGrouping'>
510        >>> seg.extractMeasureNumber()
511        >>> print(seg.brailleText)
512        ⠼⠙
513
514        Add a dot to the measure number if the segment begins mid-measure
515
516        >>> seg = braille.segment.BrailleSegment()
517        >>> seg[segKey]
518        <music21.braille.segment.BrailleElementGrouping []>
519
520        >>> seg.beginsMidMeasure = True
521        >>> seg.extractMeasureNumber()
522        >>> print(seg.brailleText)
523        ⠼⠙⠄
524        '''
525        gkp = self.groupingKeysToProcess or sorted(self.keys())
526        firstSegmentKey = gkp[0]
527        initMeasureNumber = firstSegmentKey.measure
528        brailleNumber = basic.numberToBraille(initMeasureNumber)
529        if self.beginsMidMeasure:
530            brailleNumber += symbols['dot']
531
532        self.addMeasureNumber(brailleNumber)
533
534    def extractHeading(self):
535        '''
536        Extract a :class:`~music21.key.KeySignature`, :class:`~music21.meter.TimeSignature,
537        :class:`~music21.tempo.TempoText` and :class:`~music21.tempo.MetronomeMark` and
538        add an appropriate braille heading to the brailleText object inputted.
539        '''
540        keySignature = None
541        timeSignature = None
542        tempoText = None
543        metronomeMark = None
544        # find the first keySignature and timeSignature...
545
546        groupingKeysToProcess = self.groupingKeysToProcess or sorted(self.keys())
547
548        while groupingKeysToProcess:
549            if groupingKeysToProcess[0].affinity > Affinity.MMARK:
550                break
551            cgk = groupingKeysToProcess.pop(0)  # cgk = currentGroupingKey
552
553            cgkAffinityGroup = cgk.affinity
554            currentBrailleGrouping = self._groupingDict.get(cgk)  # currentGrouping...
555
556            if cgkAffinityGroup == Affinity.SIGNATURE:
557                if len(currentBrailleGrouping) >= 2:
558                    keySignature, timeSignature = (currentBrailleGrouping[0],
559                                                   currentBrailleGrouping[1])
560                elif len(currentBrailleGrouping) == 1:
561                    keyOrTimeSig = currentBrailleGrouping[0]
562                    if isinstance(keyOrTimeSig, key.KeySignature):
563                        keySignature = keyOrTimeSig
564                    else:
565                        timeSignature = keyOrTimeSig
566            elif cgkAffinityGroup == Affinity.TTEXT:
567                tempoText = currentBrailleGrouping[0]
568            elif cgkAffinityGroup == Affinity.MMARK:
569                metronomeMark = currentBrailleGrouping[0]
570
571        if any([keySignature, timeSignature, tempoText, metronomeMark]):
572            brailleHeading = basic.transcribeHeading(
573                keySignature,
574                timeSignature,
575                tempoText,
576                metronomeMark,
577                maxLineLength=self.lineLength
578            )
579            self.addHeading(brailleHeading)
580
581
582    # def extractInaccordGrouping(self):
583    #     inaccords = self._groupingDict.get(self.currentGroupingKey)
584    #     voice_trans = []
585    #     for music21Voice in inaccords:
586    #         noteGrouping = extractBrailleElements(music21Voice)
587    #         noteGrouping.descendingChords = inaccords.descendingChords
588    #         noteGrouping.showClefSigns = inaccords.showClefSigns
589    #         noteGrouping.upperFirstInNoteFingering = inaccords.upperFirstInNoteFingering
590    #         voice_trans.append(ngMod.transcribeNoteGrouping(noteGrouping))
591    #     brailleInaccord = symbols['full_inaccord'].join(voice_trans)
592    #     self.addInaccord(brailleInaccord)
593
594
595    def extractLongExpressionGrouping(self):
596        '''
597        Extract the Long Expression that is in the ElementGrouping in cgk
598        and add it to brailleText.
599        '''
600        cgk = self.currentGroupingKey
601        currentElementGrouping = self._groupingDict.get(cgk)
602        longTextExpression = currentElementGrouping[0]
603        longExprInBraille = basic.textExpressionToBraille(longTextExpression)
604        self.addLongExpression(longExprInBraille)
605
606    def showLeadingOctaveFromNoteGrouping(self, noteGrouping):
607        '''
608        Given a noteGrouping, should we show the octave symbol?
609
610        >>> n1 = note.Note('C1')
611        >>> n2 = note.Note('D1')
612        >>> n3 = note.Note('E1')
613
614        >>> beg1 = braille.segment.BrailleElementGrouping([n1, n2, n3])
615        >>> bs1 = braille.segment.BrailleSegment()
616
617        This is True because last note is None
618
619        >>> bs1.lastNote is None
620        True
621        >>> bs1.showLeadingOctaveFromNoteGrouping(beg1)
622        True
623
624        But if we run it again, now we have a note within a fourth, so we do not
625        need to show the octave:
626
627        >>> bs1.lastNote
628        <music21.note.Note E>
629        >>> bs1.showLeadingOctaveFromNoteGrouping(beg1)
630        False
631
632        And that is true no matter how many times we call it on the same
633        BrailleElementGrouping:
634
635        >>> bs1.showLeadingOctaveFromNoteGrouping(beg1)
636        False
637
638        But if we give a new, much higher BrailleElementGrouping, we
639        will see octave marks again.
640
641        >>> nHigh1 = note.Note('C6')
642        >>> nHigh2 = note.Note('D6')
643        >>> beg2 = braille.segment.BrailleElementGrouping([nHigh1, nHigh2])
644        >>> bs1.showLeadingOctaveFromNoteGrouping(beg2)
645        True
646
647        But if we set `self.suppressOctaveMarks` to True, we won't see any
648        when we switch back to beg1:
649
650        >>> bs1.suppressOctaveMarks = True
651        >>> bs1.showLeadingOctaveFromNoteGrouping(beg2)
652        False
653
654
655        We also show octaves if for some reason two noteGroups in the same measure have
656        different BrailleElementGroupings keyed to consecutive ordinals.  The code simulates
657        that situation.
658
659        >>> bs1.suppressOctaveMarks = False
660        >>> bs1.previousGroupingKey = braille.segment.SegmentKey(measure=3, ordinal=1,
661        ...                                          affinity=braille.segment.Affinity.NOTEGROUP)
662        >>> bs1.currentGroupingKey = braille.segment.SegmentKey(measure=3, ordinal=2,
663        ...                                          affinity=braille.segment.Affinity.NOTEGROUP)
664        >>> bs1.showLeadingOctaveFromNoteGrouping(beg2)
665        True
666        >>> bs1.showLeadingOctaveFromNoteGrouping(beg1)
667        True
668        >>> bs1.showLeadingOctaveFromNoteGrouping(beg1)
669        True
670
671        '''
672        currentKey = self.currentGroupingKey
673        previousKey = self.previousGroupingKey
674
675        # if the previousKey did not exist
676        # or if the previousKey was not a collection of notes,
677        # or if the currentKey is split from the previous key for some reason
678        # while remaining in the same measure, then the lastNote is irrelevant
679        if (previousKey is not None
680                and currentKey is not None):
681            if (previousKey.affinity != Affinity.NOTEGROUP
682                or currentKey.affinity != Affinity.NOTEGROUP
683                or (currentKey.measure == previousKey.measure
684                    and currentKey.ordinal == previousKey.ordinal + 1
685                    and currentKey.hand == previousKey.hand)):
686                self.lastNote = None
687
688        if self.suppressOctaveMarks:
689            return False
690
691        # can't use Filter because noteGrouping is list-like not Stream-like
692        allNotes = [n for n in noteGrouping if isinstance(n, note.Note)]
693        showLeadingOctave = True
694        if allNotes:
695            if self.lastNote is not None:
696                firstNote = allNotes[0]
697                showLeadingOctave = basic.showOctaveWithNote(self.lastNote, firstNote)
698            # noinspection PyAttributeOutsideInit
699            self.lastNote = allNotes[-1]  # probably should not be here...
700
701        return showLeadingOctave
702
703    def needsSplitToFit(self, brailleNoteGrouping) -> bool:
704        '''
705        Returns boolean on whether a note grouping needs to be split in order to fit.
706
707        Generally a noteGrouping will need to be split if the amount of space left
708        is more than 1/4 of the line length and the brailleNoteGrouping cannot fit.
709
710        >>> n1 = note.Note('C1')
711        >>> n2 = note.Note('D1')
712        >>> n3 = note.Note('E1')
713
714        >>> beg1 = braille.segment.BrailleElementGrouping([n1, n2, n3])
715        >>> seg = braille.segment.BrailleSegment()
716        >>> seg.needsSplitToFit(beg1)
717        False
718        >>> seg.lineLength = 10
719        >>> seg.needsSplitToFit(beg1)
720        True
721        '''
722        quarterLineLength = self.lineLength // 4
723        spaceLeft = self.lineLength - self.currentLine.textLocation
724        if (spaceLeft > quarterLineLength
725                and len(brailleNoteGrouping) > quarterLineLength):
726            return True
727        else:
728            return False
729
730    def splitNoteGroupingAndTranscribe(self,
731                                       noteGrouping,
732                                       showLeadingOctaveOnFirst=False,
733                                       addSpaceToFirst=False):
734        '''
735        Take a noteGrouping and split it at a logical place,
736        returning braille transcriptions of each section.
737        '''
738        transcriber = ngMod.NoteGroupingTranscriber()
739
740        beatDivisionOffset = 0
741        REASONABLE_LIMIT = 10
742        (splitNoteGroupA, splitNoteGroupB) = (None, None)
743        brailleNoteGroupingA = None
744
745        while beatDivisionOffset < REASONABLE_LIMIT:
746            (splitNoteGroupA, splitNoteGroupB) = splitNoteGrouping(
747                noteGrouping,
748                beatDivisionOffset=beatDivisionOffset
749            )
750            transcriber.showLeadingOctave = showLeadingOctaveOnFirst
751            splitNoteGroupA.withHyphen = True
752            brailleNoteGroupingA = transcriber.transcribeGroup(splitNoteGroupA)
753            if self.currentLine.canAppend(brailleNoteGroupingA, addSpace=addSpaceToFirst):
754                break
755
756            beatDivisionOffset += 1
757            continue
758
759        showLeadingOctave = not self.suppressOctaveMarks
760        transcriber.showLeadingOctave = showLeadingOctave
761        brailleNoteGroupingB = transcriber.transcribeGroup(splitNoteGroupB)
762
763        currentKey = self.currentGroupingKey
764
765        # noinspection PyProtectedMember
766        aKey = currentKey._replace(affinity=Affinity.SPLIT1_NOTEGROUP)
767        # noinspection PyProtectedMember
768        bKey = currentKey._replace(affinity=Affinity.SPLIT2_NOTEGROUP)
769
770        self[aKey] = splitNoteGroupA
771        self[bKey] = splitNoteGroupB
772
773        return (brailleNoteGroupingA, brailleNoteGroupingB)
774
775    def extractNoteGrouping(self):
776        '''
777        Fundamentally important method that adds a noteGrouping to the braille line.
778        '''
779        transcriber = ngMod.NoteGroupingTranscriber()
780        noteGrouping = self._groupingDict.get(self.currentGroupingKey)
781
782        showLeadingOctave = self.showLeadingOctaveFromNoteGrouping(noteGrouping)
783        transcriber.showLeadingOctave = showLeadingOctave
784        brailleNoteGrouping = transcriber.transcribeGroup(noteGrouping)
785
786        addSpace = self.optionalAddKeyboardSymbolsAndDots(brailleNoteGrouping)
787
788        if self.currentLine.canAppend(brailleNoteGrouping, addSpace=addSpace):
789            self.currentLine.append(brailleNoteGrouping, addSpace=addSpace)
790        else:
791            should_split: bool = self.needsSplitToFit(brailleNoteGrouping)
792            if should_split:
793                # there is too much space left in the current line to leave it blank
794                # but not enough space left to insert the current brailleNoteGrouping
795                # hence -- let us split this noteGrouping into two noteGroupings.
796                try:
797                    bngA, bngB = self.splitNoteGroupingAndTranscribe(noteGrouping,
798                                                                 showLeadingOctave,
799                                                                 addSpace)
800                    self.currentLine.append(bngA, addSpace=addSpace)
801                    self.addToNewLine(bngB)
802                except BrailleSegmentException:
803                    # No solutions possible
804                    # Example: line length 10, chars used 7, remaining chars 3
805                    # 25% of 10 is 2, so 3 is ordinarily too much space to leave blank
806                    # But after trying to split, no solutions possible, since the first note
807                    # requires 3 chars + space = 4 chars
808                    # Give up and go to new line
809                    should_split = False
810
811            if not should_split:
812                # not enough space left on this line to use, so
813                # move the whole group to another line
814                if showLeadingOctave is False and self.suppressOctaveMarks is False:
815                    # if we didn't show the octave before, retranscribe with the octave
816                    # displayed
817                    transcriber.showLeadingOctave = True
818                    brailleNoteGrouping = transcriber.transcribeGroup(noteGrouping)
819                # if not forceHyphen:
820                self.currentLine.lastHyphenToSpace()
821                self.addToNewLine(brailleNoteGrouping)
822
823        self.addRepeatSymbols(noteGrouping.numRepeats)
824
825    def addRepeatSymbols(self, repeatTimes):
826        '''
827        Adds the appropriate number of repeat symbols, following DeGarmo chapter 17.
828
829        >>> seg = braille.segment.BrailleSegment()
830        >>> seg.addRepeatSymbols(0)
831        >>> print(seg.brailleText)
832        >>> seg.addRepeatSymbols(1)
833        >>> print(seg.brailleText)
834835
836        >>> seg = braille.segment.BrailleSegment()
837        >>> seg.addRepeatSymbols(2)
838        >>> print(seg.brailleText)
839        ⠶⠀⠶
840
841        >>> seg = braille.segment.BrailleSegment()
842        >>> seg.addRepeatSymbols(3)
843        >>> print(seg.brailleText)
844        ⠶⠼⠉
845
846        Does not yet handle situations beginning with Example 17-6 (repeats at
847        different octaves), and further
848        '''
849        if 0 < repeatTimes < 3:
850            for unused_repeatCounter in range(repeatTimes):
851                self.addSignatures(symbols['repeat'])
852        elif repeatTimes >= 3:  # 17.3 -- repeat plus number.
853            self.addSignatures(symbols['repeat'] + basic.numberToBraille(repeatTimes))
854            # noinspection PyAttributeOutsideInit
855            self.lastNote = None  # this is set up to force an octave symbol on next note
856
857    def extractSignatureGrouping(self):
858        '''
859        Extracts a key signature, time signature, and possibly an outgoing key signature
860        from the currentGroupingKey and adds it to the BrailleText object.
861        '''
862        keySignature = None
863        timeSignature = None
864
865        cgk = self.currentGroupingKey
866        noteGrouping = self._groupingDict.get(cgk)
867
868        if len(noteGrouping) >= 2:
869            keySignature, timeSignature = noteGrouping[0], noteGrouping[1]
870        elif len(noteGrouping) == 1:
871            keyOrTimeSig = self._groupingDict.get(self.currentGroupingKey)[0]
872            if isinstance(keyOrTimeSig, key.KeySignature):
873                keySignature = keyOrTimeSig
874            else:
875                timeSignature = keyOrTimeSig
876
877        outgoingKeySig = None
878        if self.cancelOutgoingKeySig and keySignature is not None:
879            try:
880                outgoingKeySig = keySignature.outgoingKeySig
881            except AttributeError:
882                pass
883
884        brailleSig = basic.transcribeSignatures(keySignature, timeSignature, outgoingKeySig)
885        if brailleSig != '':
886            self.addSignatures(brailleSig)
887
888    def extractTempoTextGrouping(self):
889        '''
890        extracts a tempo text and processes it...
891        '''
892        self.groupingKeysToProcess.insert(0, self.currentGroupingKey)
893        if self.previousGroupingKey.affinity == Affinity.SIGNATURE:
894            self.groupingKeysToProcess.insert(0, self.previousGroupingKey)
895        self.extractHeading()
896        self.extractMeasureNumber()
897
898    def consolidate(self):
899        '''
900        Puts together certain types of elements according to the last digit of their key
901        (if it is the same as Affinity.NOTEGROUP or not.
902
903        >>> SK = braille.segment.SegmentKey
904        >>> BS1 = braille.segment.BrailleSegment()
905        >>> BS1[SK(ordinal=0, affinity=2)] = ['hi', 'hello', 'there']
906        >>> BS1[SK(ordinal=1, affinity=9)] = ['these', 'get']
907        >>> BS1[SK(ordinal=2, affinity=9)] = ['put', 'together']
908        >>> BS1[SK(ordinal=3, affinity=4)] = ['in', 'new', 'group']
909        >>> BS1[SK(ordinal=4, affinity=9)] = ['with', 'the', 'previous']
910        >>> BS2 = BS1.consolidate()
911        >>> for (groupingKey, groupingList) in sorted(BS2.items()):
912        ...     print(groupingKey, groupingList)
913        SegmentKey(measure=0, ordinal=0, affinity=2, hand=None) ['hi', 'hello', 'there']
914        SegmentKey(measure=0, ordinal=1, affinity=9, hand=None) these
915        get
916        put
917        together
918        SegmentKey(measure=0, ordinal=3, affinity=4, hand=None) ['in', 'new', 'group']
919        SegmentKey(measure=0, ordinal=4, affinity=9, hand=None) with
920        the
921        previous
922        '''
923        newSegment = BrailleSegment(self.lineLength)
924        pngKey = None
925        for (groupingKey, groupingList) in sorted(self.items()):
926            if groupingKey.affinity != Affinity.NOTEGROUP:
927                newSegment[groupingKey] = groupingList
928                pngKey = None
929            else:
930                if pngKey is None:
931                    pngKey = groupingKey
932                for item in groupingList:
933                    newSegment[pngKey].append(item)
934        return newSegment
935
936    def addGroupingAttributes(self):
937        '''
938        Modifies the attributes of all :class:`~music21.braille.segment.BrailleElementGrouping`
939        instances in a list of :class:`~music21.braille.segment.BrailleSegment` instances. The
940        necessary information is retrieved from the segment and from the found clef, if any.
941        '''
942        currentKeySig = key.KeySignature(0)
943        currentTimeSig = meter.TimeSignature('4/4')
944
945        allGroupings = sorted(self.items())
946        (previousKey, previousList) = (None, None)
947
948        for (groupingKey, groupingList) in allGroupings:
949            if previousKey is not None:
950                if groupingKey.ordinal >= 1:
951                    previousList.withHyphen = True
952                if (previousKey.ordinal == 0
953                        and previousKey.affinity == Affinity.NOTEGROUP
954                        and groupingKey.ordinal == 0
955                        and groupingKey.affinity == Affinity.NOTEGROUP):
956                    if isinstance(previousList[0], clef.Clef):
957                        isRepetition = areGroupingsIdentical(previousList[1:], groupingList)
958                    else:
959                        isRepetition = areGroupingsIdentical(previousList, groupingList)
960                    if isRepetition:
961                        previousList.numRepeats += 1
962                        del self[groupingKey]
963                        continue
964            if groupingKey.affinity == Affinity.SIGNATURE:
965                for brailleElement in groupingList:
966                    if isinstance(brailleElement, meter.TimeSignature):
967                        currentTimeSig = brailleElement
968                    elif isinstance(brailleElement, key.KeySignature):
969                        brailleElement.outgoingKeySig = currentKeySig
970                        currentKeySig = brailleElement
971            elif groupingKey.affinity == Affinity.NOTEGROUP:
972                if isinstance(groupingList[0], clef.Clef):
973                    if isinstance(groupingList[0], (clef.TrebleClef, clef.AltoClef)):
974                        self.descendingChords = True
975                    elif isinstance(groupingList[0], (clef.BassClef, clef.TenorClef)):
976                        self.descendingChords = False
977
978                # make a whole rest no matter the length of the rest if only one note.
979                allGeneralNotes = [n for n in groupingList if isinstance(n, note.GeneralNote)]
980                if len(allGeneralNotes) == 1 and isinstance(allGeneralNotes[0], note.Rest):
981                    allGeneralNotes[0].fullMeasure = True
982            groupingList.keySignature = currentKeySig
983            groupingList.timeSignature = currentTimeSig
984            groupingList.descendingChords = self.descendingChords
985            groupingList.showClefSigns = self.showClefSigns
986            groupingList.upperFirstInNoteFingering = self.upperFirstInNoteFingering
987            (previousKey, previousList) = (groupingKey, groupingList)
988        if self.endHyphen:
989            previousList.withHyphen = True
990
991    def fixArticulations(self):
992        '''
993        Goes through each :class:`~music21.braille.segment.BrailleSegment` and modifies the
994        list of :attr:`~music21.note.GeneralNote.articulations` of a :class:`~music21.note.Note`
995        if appropriate. In particular, two rules are applied:
996
997        * Doubling rule => If four or more of the same :class:`~music21.articulations.Articulation`
998          are found in a row, the first instance of the articulation is doubled and the rest are
999          omitted.
1000
1001        * Staccato, Tenuto rule => "If two repeated notes appear to be tied, but either is marked
1002          staccato or tenuto, they are treated as slurred instead of tied." (BMTM, 112)
1003        '''
1004        from music21 import articulations
1005
1006        def fixOneArticulation(artic, music21NoteStart, allNotes, noteIndexStart):
1007            articName = artic.name
1008            if articName == 'fingering':  # fingerings are not considered articulations...
1009                return
1010            if (isinstance(artic, (articulations.Staccato, articulations.Tenuto))
1011                    and music21NoteStart.tie is not None):
1012                if music21NoteStart.tie.type == 'stop':
1013                    allNotes[noteIndexStart - 1].tie = None
1014                    allNotes[noteIndexStart - 1].shortSlur = True
1015                else:
1016                    allNotes[noteIndexStart + 1].tie = None
1017                    music21NoteStart.shortSlur = True
1018                music21NoteStart.tie = None
1019            numSequential = 0
1020            for noteIndexContinue in range(noteIndexStart + 1, len(allNotes)):
1021                music21NoteContinue = allNotes[noteIndexContinue]
1022                if articName in [a.name for a in music21NoteContinue.articulations]:
1023                    numSequential += 1
1024                    continue
1025                break
1026            if numSequential < 3:
1027                return
1028            # else:
1029            # double the articulation on the first note and remove from the next...
1030            music21NoteStart.articulations.append(artic)
1031            for noteIndexContinue in range(noteIndexStart + 1,
1032                                           noteIndexStart + numSequential):
1033                music21NoteContinue = allNotes[noteIndexContinue]
1034                for artOther in music21NoteContinue.articulations:
1035                    if artOther.name == articName:
1036                        music21NoteContinue.articulations.remove(artOther)
1037
1038        newSegment = self.consolidate()
1039        noteGroupings = [newSegment[gpKey]
1040                             for gpKey in newSegment.keys()
1041                                if gpKey.affinity == Affinity.NOTEGROUP]
1042        for noteGrouping in noteGroupings:
1043            allNotes_outer = [n for n in noteGrouping if isinstance(n, note.Note)]
1044            for noteIndexStart_outer in range(len(allNotes_outer)):
1045                music21NoteStart_outer = allNotes_outer[noteIndexStart_outer]
1046                for artic_outer in music21NoteStart_outer.articulations:
1047                    fixOneArticulation(
1048                        artic_outer,
1049                        music21NoteStart_outer,
1050                        allNotes_outer,
1051                        noteIndexStart_outer
1052                    )
1053
1054
1055class BrailleGrandSegment(BrailleSegment, text.BrailleKeyboard):
1056    '''
1057    A BrailleGrandSegment represents a pair of segments (rightSegment, leftSegment)
1058    representing the right and left hands of a piano staff (or other two-staff object).
1059
1060    >>> bgs = braille.segment.BrailleGrandSegment(lineLength=36)
1061    >>> bgs.lineLength
1062    36
1063    '''
1064    def __init__(self, lineLength: int = 40):
1065        BrailleSegment.__init__(self, lineLength=lineLength)
1066        text.BrailleKeyboard.__init__(self, lineLength=lineLength)
1067        self.allKeyPairs = []
1068        self.previousGroupingPair = None
1069        self.currentGroupingPair = None
1070
1071    @property
1072    def brailleText(self):
1073        return text.BrailleKeyboard.__str__(self)
1074
1075    def __str__(self):
1076        name = '<music21.braille.segment BrailleGrandSegment>\n==='
1077        allPairs = []
1078        for (rightKey, leftKey) in self.yieldCombinedGroupingKeys():
1079            if rightKey is not None:
1080                rightHeading = 'Measure {0} Right, {1} {2}:\n'.format(
1081                    rightKey.measure, affinityNames[rightKey.affinity], rightKey.ordinal + 1)
1082                rightContents = str(self._groupingDict.get(rightKey))
1083                rightFull = ''.join([rightHeading, rightContents])
1084            else:
1085                rightFull = ''
1086            if leftKey is not None:
1087                leftHeading = '\nMeasure {0} Left, {1} {2}:\n'.format(
1088                    leftKey.measure, affinityNames[leftKey.affinity], leftKey.ordinal + 1)
1089                leftContents = str(self._groupingDict.get(leftKey))
1090                leftFull = ''.join([leftHeading, leftContents])
1091            else:
1092                leftFull = ''
1093            allPairs.append('\n'.join([rightFull, leftFull, '====\n']))
1094        out = '\n'.join(['---begin grand segment---', name, ''.join(allPairs),
1095                           '---end grand segment---'])
1096        return out
1097
1098    def yieldCombinedGroupingKeys(self):
1099        '''
1100        yields all the keys in order as a tuple of (rightKey, leftKey) where
1101        two keys are grouped if they have the same segmentKey except for the hand.
1102
1103        >>> bgs = braille.segment.BrailleGrandSegment()
1104        >>> SegmentKey = braille.segment.SegmentKey  # namedtuple
1105        >>> bgs[SegmentKey(1, 1, 1, 'right')] = '1r'
1106        >>> bgs[SegmentKey(1, 1, 1, 'left')]  = '1l'
1107        >>> bgs[SegmentKey(1, 2, 3, 'right')] = '2r'
1108        >>> bgs[SegmentKey(1, 2, 4, 'left')] = '3l'
1109        >>> bgs[SegmentKey(2, 1, 9, 'left')] = '4l'
1110        >>> bgs[SegmentKey(2, 1, 9, 'right')] = '4r'
1111        >>> bgs[SegmentKey(3, 1, 9, 'right')] = '5r'
1112        >>> for l, r in bgs.yieldCombinedGroupingKeys():
1113        ...     (bgs[l], bgs[r])
1114        ('1r', '1l')
1115        ('2r', <music21.braille.segment.BrailleElementGrouping []>)
1116        (<music21.braille.segment.BrailleElementGrouping []>, '3l')
1117        ('4r', '4l')
1118        ('5r', <music21.braille.segment.BrailleElementGrouping []>)
1119        '''
1120        def segmentKeySortKey(segmentKey):
1121            '''
1122            sort by measure, then ordinal, then affinity, then hand (r then l)
1123            '''
1124            if segmentKey.hand == 'right':
1125                skH = -1
1126            else:
1127                skH = 1
1128            return (segmentKey.measure, segmentKey.ordinal, segmentKey.affinity, skH)
1129
1130        def matchOther(thisKey_inner, otherKey):
1131            if (thisKey_inner.measure == otherKey.measure
1132                    and thisKey_inner.ordinal == otherKey.ordinal
1133                    and thisKey_inner.affinity == otherKey.affinity):
1134                return True
1135            else:
1136                return False
1137
1138        storedRight = None
1139        storedLeft = None
1140        for thisKey in sorted(self.keys(), key=segmentKeySortKey):
1141            if thisKey.hand == 'right':
1142                if storedLeft is not None:
1143                    if matchOther(thisKey, storedLeft):
1144                        yield(thisKey, storedLeft)
1145                    elif (thisKey.affinity == Affinity.NOTEGROUP
1146                          and matchOther(thisKey._replace(affinity=Affinity.INACCORD), storedLeft)):
1147                        # r.h. notegroup goes before an lh inaccord, despite this being out of order
1148                        yield(thisKey, storedLeft)
1149                    else:
1150                        yield(None, storedLeft)
1151                        storedRight = thisKey
1152                    storedLeft = None
1153                else:
1154                    storedRight = thisKey
1155            elif thisKey.hand == 'left':
1156                if storedRight is not None:
1157                    if matchOther(thisKey, storedRight):
1158                        yield(storedRight, thisKey)
1159                    elif storedRight.affinity < Affinity.INACCORD:
1160                        yield(storedRight, None)
1161                        yield(None, thisKey)
1162                    else:
1163                        yield(storedRight, None)
1164                        storedLeft = thisKey
1165                    storedRight = None
1166                else:
1167                    storedLeft = thisKey
1168
1169        if storedRight:
1170            yield (storedRight, None)
1171        if storedLeft:
1172            yield (None, storedLeft)
1173
1174    # def combineGroupingKeys(self, rightSegment, leftSegment):
1175    #     # return list(self.yieldCombinedGroupingKeys())
1176    #
1177    #     groupingKeysRight = sorted(rightSegment.keys())
1178    #     groupingKeysLeft = sorted(leftSegment.keys())
1179    #     combinedGroupingKeys = []
1180    #
1181    #     while groupingKeysRight:
1182    #         gkRight = groupingKeysRight.pop(0)
1183    #         try:
1184    #             groupingKeysLeft.remove(gkRight)
1185    #             combinedGroupingKeys.append((gkRight, gkRight))
1186    #         except ValueError:
1187    #             if gkRight.affinity < Affinity.INACCORD:
1188    #                 combinedGroupingKeys.append((gkRight, None))
1189    #             else:
1190    #                 if gkRight.affinity == Affinity.INACCORD:
1191    #                     gkLeft = gkRight._replace(affinity=gkRight.affinity + 1)
1192    #                 else:
1193    #                     gkLeft = gkRight._replace(affinity=gkRight.affinity - 1)
1194    #                 try:
1195    #                     groupingKeysLeft.remove(gkLeft)
1196    #                 except ValueError:
1197    #                     raise BrailleSegmentException(
1198    #                         'Misaligned braille groupings: ' +
1199    #                         'groupingKeyLeft was %s' % gkLeft +
1200    #                         'groupingKeyRight was %s' % gkRight +
1201    #                         'rightSegment was %s, leftSegment was %s' %
1202    #                                    (rightSegment, leftSegment))
1203    #
1204    #                 try:
1205    #                     combinedGroupingTuple = (gkRight, gkLeft)
1206    #                     combinedGroupingKeys.append(combinedGroupingTuple)
1207    #                 except ValueError:
1208    #                     raise BrailleSegmentException(
1209    #                         'Misaligned braille groupings could not append combinedGroupingKeys')
1210    #
1211    #
1212    #     while groupingKeysLeft:
1213    #         gkLeft = groupingKeysLeft.pop(0)
1214    #         combinedGroupingTuple = (None, gkLeft)
1215    #         combinedGroupingKeys.append(combinedGroupingTuple)
1216    #
1217    #     return combinedGroupingKeys
1218
1219
1220    def transcribe(self):
1221        '''
1222        Returns the BrailleText from the combined grouping keys
1223        '''
1224        self.allKeyPairs = list(self.yieldCombinedGroupingKeys())
1225        lastPair = self.allKeyPairs[-1]
1226        highestMeasure = lastPair[0].measure if lastPair[0] else lastPair[1].measure
1227        self.highestMeasureNumberLength = len(str(highestMeasure))
1228
1229        self.extractHeading()  # Heading
1230        self.currentGroupingPair = None
1231        while self.allKeyPairs:
1232            self.previousGroupingPair = self.currentGroupingPair
1233            self.currentGroupingPair = self.allKeyPairs.pop(0)
1234            (rightKey, leftKey) = self.currentGroupingPair
1235
1236            if ((rightKey is not None and rightKey.affinity >= Affinity.INACCORD)
1237                    or (leftKey is not None and leftKey.affinity >= Affinity.INACCORD)):
1238                self.extractNoteGrouping()  # Note or Inaccord Grouping
1239            # elif (rightKey.affinity == Affinity.SIGNATURE
1240            #        or leftKey.affinity == Affinity.SIGNATURE):
1241            #     self.extractSignatureGrouping()  # Signature Grouping
1242            # elif (rightKey.affinity == Affinity.LONG_TEXTEXPR
1243            #        or leftKey.affinity == Affinity.LONG_TEXTEXPR):
1244            #     self.extractLongExpressionGrouping()  # Long Expression Grouping
1245            # elif rightKey.affinity == Affinity.TTEXT or leftKey.affinity == Affinity.TTEXT:
1246            #     self.extractTempoTextGrouping()  # Tempo Text Grouping
1247        return self.brailleText
1248
1249    def extractHeading(self):
1250        '''
1251        Finds KeySignatures, TimeSignatures, TempoText, and Metronome Marks
1252        within the keyPairs, and removes some from allKeyPairs.
1253        '''
1254        keySignature = None
1255        timeSignature = None
1256        tempoText = None
1257        metronomeMark = None
1258
1259        while True:
1260            (rightKey, leftKey) = self.allKeyPairs[0]
1261            useKey = rightKey
1262            try:
1263                useElement = self._groupingDict.get(rightKey)
1264            except KeyError as ke:
1265                if ke.args[0] == 'None':
1266                    useElement = self._groupingDict.get(leftKey)
1267                    useKey = leftKey
1268                else:
1269                    raise ke
1270            if useKey.affinity > Affinity.MMARK:
1271                break
1272            self.allKeyPairs.pop(0)
1273            if useKey.affinity == Affinity.SIGNATURE:
1274                try:
1275                    keySignature, timeSignature = useElement[0], useElement[1]
1276                except IndexError:
1277                    if isinstance(useElement, key.KeySignature):
1278                        keySignature = useElement[0]
1279                    else:
1280                        timeSignature = useElement[0]
1281            elif useKey.affinity == Affinity.TTEXT:
1282                tempoText = useElement[0]
1283            elif useKey.affinity == Affinity.MMARK:
1284                metronomeMark = useElement[0]
1285
1286        try:
1287            brailleHeading = basic.transcribeHeading(
1288                keySignature,
1289                timeSignature,
1290                tempoText,
1291                metronomeMark,
1292                maxLineLength=self.lineLength
1293            )
1294            self.addHeading(brailleHeading)
1295        except basic.BrailleBasicException as bbe:
1296            if bbe.args[0] != 'No heading can be made.':
1297                raise bbe
1298
1299    def extractNoteGrouping(self):
1300        (rightKey, leftKey) = self.currentGroupingPair
1301        if rightKey:
1302            mNum = rightKey.measure
1303        elif leftKey:
1304            mNum = leftKey.measure
1305        else:
1306            raise ValueError('Measure must be defined for leftKey or rightKey')
1307
1308        currentMeasureNumber = basic.numberToBraille(mNum, withNumberSign=False)
1309
1310        def brailleFromKey(rightOrLeftKey):
1311            if rightOrLeftKey is not None and rightOrLeftKey.affinity == Affinity.INACCORD:
1312                inaccords = self._groupingDict.get(rightOrLeftKey)
1313                voice_trans = []
1314                for music21Voice in inaccords:
1315                    noteGrouping = extractBrailleElements(music21Voice)
1316                    noteGrouping.descendingChords = inaccords.descendingChords
1317                    noteGrouping.showClefSigns = inaccords.showClefSigns
1318                    noteGrouping.upperFirstInNoteFingering = inaccords.upperFirstInNoteFingering
1319                    voice_trans.append(ngMod.transcribeNoteGrouping(noteGrouping))
1320                brailleStr = symbols['full_inaccord'].join(voice_trans)
1321            elif rightOrLeftKey is not None:
1322                brailleStr = ngMod.transcribeNoteGrouping(self._groupingDict.get(rightOrLeftKey))
1323            else:
1324                brailleStr = ''
1325
1326            return brailleStr
1327
1328        rhBraille = brailleFromKey(rightKey)
1329        lhBraille = brailleFromKey(leftKey)
1330
1331        self.addNoteGroupings(currentMeasureNumber, rhBraille, lhBraille)
1332
1333    # # noinspection PyUnusedLocal
1334    # def extractSignatureGrouping(self, brailleKeyboard):
1335    #     pass
1336    #
1337    # # noinspection PyUnusedLocal
1338    # def extractLongExpressionGrouping(self, brailleKeyboard):
1339    #     pass
1340    #
1341    # # noinspection PyUnusedLocal
1342    # def extractTempoTextGrouping(self, brailleKeyboard):
1343    #     pass
1344
1345
1346# ------------------------------------------------------------------------------
1347# Grouping + Segment creation from music21.stream Part
1348
1349def findSegments(music21Part,
1350                 *,
1351                 setHand=None,
1352                 cancelOutgoingKeySig=True,
1353                 descendingChords=None,
1354                 dummyRestLength=None,
1355                 maxLineLength=40,
1356                 segmentBreaks=None,
1357                 showClefSigns=False,
1358                 showFirstMeasureNumber=True,
1359                 showHand=None,
1360                 showHeading=True,
1361                 showLongSlursAndTiesTogether: Optional[bool] = None,
1362                 showShortSlursAndTiesTogether=False,
1363                 slurLongPhraseWithBrackets=True,
1364                 suppressOctaveMarks=False,
1365                 upperFirstInNoteFingering=True,
1366                 ):
1367    '''
1368    Takes in a :class:`~music21.stream.Part`.
1369
1370    Returns a list of :class:`~music21.segment.BrailleSegment` instances.
1371
1372    Five functions or methods get called in the generation of segments:
1373
1374    * :func:`~music21.braille.segment.prepareSlurredNotes`
1375    * :func:`~music21.braille.segment.getRawSegments`
1376    * :meth:`~music21.braille.segment.BrailleSegment.addGroupingAttributes`
1377    * :meth:`~music21.braille.segment.BrailleSegment.addSegmentAttributes`
1378    * :meth:`~music21.braille.segment.BrailleSegment.fixArticulations`
1379
1380    >>> from music21.braille import test
1381    >>> example = test.example11_2()
1382    >>> allSegments = braille.segment.findSegments(example)
1383
1384    >>> print(str(allSegments[0]))
1385    ---begin segment---
1386    <music21.braille.segment BrailleSegment>
1387    Measure 0, Signature Grouping 1:
1388    <music21.key.KeySignature of 3 flats>
1389    <music21.meter.TimeSignature 4/4>
1390    ===
1391    Measure 0, Note Grouping 1:
1392    <music21.clef.TrebleClef>
1393    <music21.note.Note B->
1394    ===
1395    Measure 1, Note Grouping 1:
1396    <music21.note.Note G>
1397    <music21.note.Note E->
1398    <music21.note.Note D>
1399    <music21.note.Note E->
1400    ===
1401    Measure 2, Note Grouping 1:
1402    <music21.note.Note G>
1403    <music21.note.Note F>
1404    <music21.note.Note E->
1405    ===
1406    Measure 3, Note Grouping 1:
1407    <music21.note.Note A->
1408    <music21.note.Note G>
1409    <music21.note.Note C>
1410    <music21.note.Note C>
1411    ===
1412    Measure 4, Note Grouping 1:
1413    <music21.note.Note B->
1414    <music21.note.Note B->
1415    ===
1416    Measure 5, Note Grouping 1:
1417    <music21.note.Note E->
1418    <music21.note.Note B->
1419    <music21.note.Note A->
1420    <music21.note.Note G>
1421    ===
1422    Measure 6, Note Grouping 1:
1423    <music21.note.Note G>
1424    <music21.note.Note F>
1425    <music21.note.Note C>
1426    ===
1427    Measure 7, Note Grouping 1:
1428    <music21.note.Note C>
1429    <music21.note.Note F>
1430    <music21.note.Note A->
1431    <music21.note.Note D>
1432    ===
1433    Measure 8, Note Grouping 1:
1434    <music21.note.Note E->
1435    music hyphen ⠐
1436    ===
1437    ---end segment---
1438
1439
1440    Second segment
1441
1442    >>> print(str(allSegments[1]))
1443    ---begin segment---
1444    <music21.braille.segment BrailleSegment>
1445    Measure 8, Note Grouping 2:
1446    <music21.note.Note G>
1447    ===
1448    Measure 9, Note Grouping 1:
1449    <music21.note.Note G>
1450    <music21.note.Note F>
1451    <music21.note.Note F>
1452    <music21.note.Note F>
1453    ===
1454    Measure 10, Note Grouping 1:
1455    <music21.note.Note A->
1456    <music21.note.Note G>
1457    <music21.note.Note B->
1458    ===
1459    Measure 11, Note Grouping 1:
1460    <music21.note.Note B->
1461    <music21.note.Note A>
1462    <music21.note.Note A>
1463    <music21.note.Note C>
1464    ===
1465    Measure 12, Note Grouping 1:
1466    <music21.note.Note B->
1467    <music21.note.Note B->
1468    ===
1469    Measure 13, Note Grouping 1:
1470    <music21.note.Note E->
1471    <music21.note.Note B->
1472    <music21.note.Note A->
1473    <music21.note.Note G>
1474    ===
1475    Measure 14, Note Grouping 1:
1476    <music21.note.Note G>
1477    <music21.note.Note F>
1478    <music21.note.Note C>
1479    ===
1480    Measure 15, Note Grouping 1:
1481    <music21.note.Note C>
1482    <music21.note.Rest quarter>
1483    <music21.note.Note F>
1484    <music21.note.Rest quarter>
1485    ===
1486    Measure 16, Note Grouping 1:
1487    <music21.note.Note A->
1488    <music21.note.Note D>
1489    ===
1490    Measure 17, Note Grouping 1:
1491    <music21.note.Note E->
1492    <music21.bar.Barline type=final>
1493    ===
1494    ---end segment---
1495    '''
1496    # Slurring
1497    # --------
1498    prepareSlurredNotes(music21Part,
1499                        showLongSlursAndTiesTogether=showLongSlursAndTiesTogether,
1500                        showShortSlursAndTiesTogether=showShortSlursAndTiesTogether,
1501                        slurLongPhraseWithBrackets=slurLongPhraseWithBrackets,
1502                        )
1503
1504    # Raw Segments
1505    # ------------
1506    allSegments = getRawSegments(music21Part, setHand=setHand, maxLineLength=maxLineLength)
1507
1508    for seg in allSegments:
1509        # Grouping Attributes
1510        # -------------------
1511        seg.showClefSigns = showClefSigns
1512        seg.upperFirstInNoteFingering = upperFirstInNoteFingering
1513        seg.descendingChords = descendingChords
1514        seg.addGroupingAttributes()
1515
1516        # Segment Attributes
1517        # ------------------
1518        seg.cancelOutgoingKeySig = cancelOutgoingKeySig
1519        seg.dummyRestLength = dummyRestLength
1520        seg.showFirstMeasureNumber = showFirstMeasureNumber
1521        seg.showHand = showHand
1522        seg.showHeading = showHeading
1523        seg.suppressOctaveMarks = suppressOctaveMarks
1524
1525        # Articulations
1526        # -------------
1527        seg.fixArticulations()
1528
1529    return allSegments
1530
1531
1532def prepareSlurredNotes(music21Part,
1533                        *,
1534                        slurLongPhraseWithBrackets=True,
1535                        showShortSlursAndTiesTogether=False,
1536                        showLongSlursAndTiesTogether: Optional[bool] = None,
1537                        ):
1538    '''
1539    Takes in a :class:`~music21.stream.Part` and three keywords:
1540
1541    * slurLongPhraseWithBrackets
1542    * showShortSlursAndTiesTogether
1543    * showLongSlursAndTiesTogether
1544
1545    For any slurs present in the Part, the appropriate notes are labeled
1546    with attributes indicating where to put the symbols that represent
1547    slurring in braille. For purposes of slurring in braille, there is
1548    a distinction between short and long phrases. In a short phrase, a
1549    slur covers up to four notes. A short slur symbol should follow each
1550    note except the last.
1551
1552
1553    >>> import copy
1554    >>> from music21.braille import segment
1555    >>> short = converter.parse('tinynotation: 3/4 c4 d e')
1556    >>> s1 = spanner.Slur(short.recurse().notes.first(), short.recurse().notes.last())
1557    >>> short.append(s1)
1558    >>> short.show('text')
1559    {0.0} <music21.stream.Measure 1 offset=0.0>
1560        {0.0} <music21.clef.TrebleClef>
1561        {0.0} <music21.meter.TimeSignature 3/4>
1562        {0.0} <music21.note.Note C>
1563        {1.0} <music21.note.Note D>
1564        {2.0} <music21.note.Note E>
1565        {3.0} <music21.bar.Barline type=final>
1566    {3.0} <music21.spanner.Slur <music21.note.Note C><music21.note.Note E>>
1567    >>> shortA = copy.deepcopy(short)
1568    >>> segment.prepareSlurredNotes(shortA)
1569    >>> shortA.recurse().notes[0].shortSlur
1570    True
1571    >>> shortA.recurse().notes[1].shortSlur
1572    True
1573
1574
1575    In a long phrase, a slur covers more than four notes. There are two
1576    options for slurring long phrases. The first is by using the bracket
1577    slur. By default, slurLongPhraseWithBrackets is True. The opening
1578    bracket sign is put before the first note, and the closing bracket
1579    sign is put before the last note.
1580
1581
1582    >>> long = converter.parse('tinynotation: 3/4 c8 d e f g a')
1583    >>> s2 = spanner.Slur(long[note.Note].first(), long[note.Note].last())
1584    >>> long.append(s2)
1585    >>> long.show('text')
1586    {0.0} <music21.stream.Measure 1 offset=0.0>
1587        {0.0} <music21.clef.TrebleClef>
1588        {0.0} <music21.meter.TimeSignature 3/4>
1589        {0.0} <music21.note.Note C>
1590        {0.5} <music21.note.Note D>
1591        {1.0} <music21.note.Note E>
1592        {1.5} <music21.note.Note F>
1593        {2.0} <music21.note.Note G>
1594        {2.5} <music21.note.Note A>
1595        {3.0} <music21.bar.Barline type=final>
1596    {3.0} <music21.spanner.Slur <music21.note.Note C><music21.note.Note A>>
1597    >>> longA = copy.deepcopy(long)
1598    >>> segment.prepareSlurredNotes(longA)
1599    >>> longA[note.Note].first().beginLongBracketSlur
1600    True
1601    >>> longA[note.Note].last().endLongBracketSlur
1602    True
1603
1604
1605    The other way is by using the double slur, setting `slurLongPhraseWithBrackets`
1606    to False. The opening sign of the double slur is put after the first note
1607    (i.e. before the second note) and the closing sign is put before the last
1608    note (i.e. before the second to last note).
1609
1610    If a value is not supplied for `showLongSlursAndTiesTogether`, it will take the
1611    value set for `slurLongPhraseWithBrackets`, which defaults True.
1612
1613
1614    >>> longB = copy.deepcopy(long)
1615    >>> segment.prepareSlurredNotes(longB, slurLongPhraseWithBrackets=False)
1616    >>> longB.recurse().notes[1].beginLongDoubleSlur
1617    True
1618    >>> longB.recurse().notes[-2].endLongDoubleSlur
1619    True
1620
1621
1622    In the event that slurs and ties are shown together in print, the slur is
1623    redundant. Examples are shown for slurring a short phrase; the process is
1624    identical for slurring a long phrase.
1625
1626
1627    Below, a tie has been added between the first two notes of the short phrase
1628    defined above. If showShortSlursAndTiesTogether is set to its default value of
1629    False, then the slur on either side of the phrase is reduced by the amount that
1630    ties are present, as shown below.
1631
1632
1633    >>> short.recurse().notes[0].tie = tie.Tie('start')
1634    >>> shortB = copy.deepcopy(short)
1635    >>> segment.prepareSlurredNotes(shortB)
1636    >>> shortB.recurse().notes[0].shortSlur
1637    Traceback (most recent call last):
1638    AttributeError: 'Note' object has no attribute 'shortSlur'
1639    >>> shortB.recurse().notes[0].tie
1640    <music21.tie.Tie start>
1641    >>> shortB.recurse().notes[1].shortSlur
1642    True
1643
1644
1645    If showShortSlursAndTiesTogether is set to True, then the slurs and ties are
1646    shown together (i.e. the note has both a shortSlur and a tie).
1647
1648    >>> shortC = copy.deepcopy(short)
1649    >>> segment.prepareSlurredNotes(shortC, showShortSlursAndTiesTogether=True)
1650    >>> shortC.recurse().notes[0].shortSlur
1651    True
1652    >>> shortC.recurse().notes[0].tie
1653    <music21.tie.Tie start>
1654
1655    TODO: This should not add attributes to Note objects but instead return a collection
1656    of sets of notes that have each element applied to it.
1657    '''
1658    if not music21Part.spannerBundle:
1659        return
1660    if showLongSlursAndTiesTogether is None:
1661        showLongSlursAndTiesTogether = slurLongPhraseWithBrackets
1662
1663    allNotes = music21Part.flatten().notes.stream()
1664    for slur in music21Part.spannerBundle.getByClass(spanner.Slur):
1665        firstNote = slur.getFirst()
1666        lastNote = slur.getLast()
1667
1668        try:
1669            beginIndex = allNotes.index(firstNote)
1670            endIndex = allNotes.index(lastNote)
1671        except exceptions21.StreamException:  # pragma: no cover
1672            # there might be a case where a slur is present in a Stream but where
1673            # the elements of the slur are no longer present in the stream,
1674            # such as if they were manually removed.  It is rare, but why this is here.
1675            continue
1676
1677        delta = abs(endIndex - beginIndex) + 1
1678
1679        if not showShortSlursAndTiesTogether and delta <= SEGMENT_MAXNOTESFORSHORTSLUR:
1680            # normally slurs are not shown on tied notes (unless
1681            # showShortSlursAndTiesTogether is True, for facsimile transcriptions).
1682            if (allNotes[beginIndex].tie is not None
1683                    and allNotes[beginIndex].tie.type == 'start'):
1684                beginIndex += 1
1685            if allNotes[endIndex].tie is not None and allNotes[endIndex].tie.type == 'stop':
1686                endIndex -= 1
1687
1688        if not showLongSlursAndTiesTogether and delta > SEGMENT_MAXNOTESFORSHORTSLUR:
1689            if (allNotes[beginIndex].tie is not None
1690                    and allNotes[beginIndex].tie.type == 'start'):
1691                beginIndex += 1
1692            if allNotes[endIndex].tie is not None and allNotes[endIndex].tie.type == 'stop':
1693                endIndex -= 1
1694
1695        if delta <= SEGMENT_MAXNOTESFORSHORTSLUR:
1696            for noteIndex in range(beginIndex, endIndex):
1697                allNotes[noteIndex].shortSlur = True
1698        else:
1699            if slurLongPhraseWithBrackets:
1700                allNotes[beginIndex].beginLongBracketSlur = True
1701                allNotes[endIndex].endLongBracketSlur = True
1702            else:
1703                allNotes[beginIndex + 1].beginLongDoubleSlur = True
1704                allNotes[endIndex - 1].endLongDoubleSlur = True
1705
1706
1707def getRawSegments(music21Part,
1708                   *,
1709                   setHand=None,
1710                   maxLineLength: int = 40,
1711                   ):
1712    '''
1713    Takes in a :class:`~music21.stream.Part`, divides it up into segments (i.e. instances of
1714    :class:`~music21.braille.segment.BrailleSegment`). This function assumes
1715    that the Part is already divided up into measures
1716    (see :class:`~music21.stream.Measure`). An acceptable input is shown below.
1717
1718    This will automatically find appropriate segment breaks at
1719    :class:`~music21.braille.objects.BrailleSegmentDivision`
1720    or :class:`~music21.braille.objects.BrailleOptionalSegmentDivision`
1721    or after 48 elements if a double bar or repeat sign is encountered.
1722
1723    Two functions are called for each measure during the creation of segments:
1724
1725    * :func:`~music21.braille.segment.prepareBeamedNotes`
1726    * :func:`~music21.braille.segment.extractBrailleElements`
1727
1728    >>> tn = converter.parse("tinynotation: 3/4 c4 c c e e e g g g c'2.")
1729    >>> tn = tn.makeNotation(cautionaryNotImmediateRepeat=False)
1730    >>> tn.show('text')
1731    {0.0} <music21.stream.Measure 1 offset=0.0>
1732        {0.0} <music21.clef.TrebleClef>
1733        {0.0} <music21.meter.TimeSignature 3/4>
1734        {0.0} <music21.note.Note C>
1735        {1.0} <music21.note.Note C>
1736        {2.0} <music21.note.Note C>
1737    {3.0} <music21.stream.Measure 2 offset=3.0>
1738        {0.0} <music21.note.Note E>
1739        {1.0} <music21.note.Note E>
1740        {2.0} <music21.note.Note E>
1741    {6.0} <music21.stream.Measure 3 offset=6.0>
1742        {0.0} <music21.note.Note G>
1743        {1.0} <music21.note.Note G>
1744        {2.0} <music21.note.Note G>
1745    {9.0} <music21.stream.Measure 4 offset=9.0>
1746        {0.0} <music21.note.Note C>
1747        {3.0} <music21.bar.Barline type=final>
1748
1749    By default, there is no break anywhere within the Part,
1750    and a segmentList of size 1 is returned.
1751
1752    >>> import copy
1753    >>> from music21.braille import segment
1754    >>> tnA = copy.deepcopy(tn)
1755    >>> rawSegments = segment.getRawSegments(tnA)
1756    >>> len(rawSegments)
1757    1
1758    >>> rawSegments[0]
1759    <music21.braille.segment.BrailleSegment 1 line, 0 headings, 40 cols>
1760
1761    >>> print(rawSegments[0])
1762    ---begin segment---
1763    <music21.braille.segment BrailleSegment>
1764    Measure 1, Signature Grouping 1:
1765    <music21.meter.TimeSignature 3/4>
1766    ===
1767    Measure 1, Note Grouping 1:
1768    <music21.clef.TrebleClef>
1769    <music21.note.Note C>
1770    <music21.note.Note C>
1771    <music21.note.Note C>
1772    ===
1773    Measure 2, Note Grouping 1:
1774    <music21.note.Note E>
1775    <music21.note.Note E>
1776    <music21.note.Note E>
1777    ===
1778    Measure 3, Note Grouping 1:
1779    <music21.note.Note G>
1780    <music21.note.Note G>
1781    <music21.note.Note G>
1782    ===
1783    Measure 4, Note Grouping 1:
1784    <music21.note.Note C>
1785    <music21.bar.Barline type=final>
1786    ===
1787    ---end segment---
1788
1789    Now, a segment break occurs at measure 2, offset 1.0 within that measure.
1790    The two segments are shown below.
1791
1792    >>> tnB = copy.deepcopy(tn)
1793    >>> tnB.measure(2).insert(1.0, braille.objects.BrailleSegmentDivision())
1794    >>> allSegments = segment.getRawSegments(tnB)
1795    >>> len(allSegments)
1796    2
1797
1798    >>> allSegments[0]
1799    <music21.braille.segment.BrailleSegment 1 line, 0 headings, 40 cols>
1800
1801    >>> print(allSegments[0])
1802    ---begin segment---
1803    <music21.braille.segment BrailleSegment>
1804    Measure 1, Signature Grouping 1:
1805    <music21.meter.TimeSignature 3/4>
1806    ===
1807    Measure 1, Note Grouping 1:
1808    <music21.clef.TrebleClef>
1809    <music21.note.Note C>
1810    <music21.note.Note C>
1811    <music21.note.Note C>
1812    ===
1813    Measure 2, Note Grouping 1:
1814    <music21.note.Note E>
1815    ===
1816    ---end segment---
1817
1818    >>> allSegments[1]
1819    <music21.braille.segment.BrailleSegment 1 line, 0 headings, 40 cols>
1820
1821    >>> print(allSegments[1])
1822    ---begin segment---
1823    <music21.braille.segment BrailleSegment>
1824    Measure 2, Note Grouping 2:
1825    <music21.note.Note E>
1826    <music21.note.Note E>
1827    ===
1828    Measure 3, Note Grouping 1:
1829    <music21.note.Note G>
1830    <music21.note.Note G>
1831    <music21.note.Note G>
1832    ===
1833    Measure 4, Note Grouping 1:
1834    <music21.note.Note C>
1835    <music21.bar.Barline type=final>
1836    ===
1837    ---end segment---
1838
1839    If we insert an optional division, the division
1840    only appears if there are 48 elements in the current segment:
1841
1842    >>> tnC = copy.deepcopy(tn)
1843    >>> tnC.measure(1).insert(1.0, braille.objects.BrailleOptionalSegmentDivision())
1844    >>> allSegments = segment.getRawSegments(tnC)
1845    >>> len(allSegments)
1846    1
1847
1848    If by happenstance a segment division object is encountered where a division
1849    has just been created (or the very beginning),
1850    no unnecessary empty segment will be created:
1851
1852    >>> tnD = copy.deepcopy(tn)
1853    >>> tnD.measure(1).insert(0, braille.objects.BrailleSegmentDivision())
1854    >>> allSegments = segment.getRawSegments(tnD)
1855    >>> len(allSegments)
1856    1
1857    '''
1858    allSegments = []
1859
1860    currentSegment = BrailleSegment(lineLength=maxLineLength)
1861
1862    elementsInCurrentSegment: int = 0
1863
1864    startANewSegment: bool = False
1865
1866    # TODO: why is this skipping the measure layer and getting voices?
1867    for music21Measure in music21Part.getElementsByClass([stream.Measure, stream.Voice]):
1868        prepareBeamedNotes(music21Measure)
1869        brailleElements = extractBrailleElements(music21Measure)
1870        ordinal: int = 0
1871        previousAffinityCode = Affinity._LOWEST  # -1
1872
1873        for brailleElement in brailleElements:
1874            if startANewSegment:
1875                # Dispose of existing segment
1876                if brailleElement.offset != 0.0:
1877                    # Correct use of end hyphen depends on knowledge of line length (Ex. 10-5)
1878                    # So just be conservative and add it
1879                    currentSegment.endHyphen = True
1880
1881                # Start new segment and increment number if this is a midmeasure segment start
1882                allSegments.append(currentSegment)
1883                currentSegment = BrailleSegment(lineLength=maxLineLength)
1884                if brailleElement.offset != 0.0:
1885                    currentSegment.beginsMidMeasure = True
1886
1887                elementsInCurrentSegment = 0
1888                if previousAffinityCode is not Affinity._LOWEST:
1889                    ordinal += 1
1890
1891                startANewSegment = False
1892            if 'BrailleSegmentDivision' in brailleElement.classes:
1893                if ('BrailleOptionalSegmentDivision' in brailleElement.classes
1894                        and elementsInCurrentSegment <= MAX_ELEMENTS_IN_SEGMENT):
1895                    # Optional condition not met -- pass
1896                    pass
1897                # Optional condition met, or required
1898                elif elementsInCurrentSegment > 0:
1899                    startANewSegment = True
1900                # All cases: continue, so we don't add anything to the segment
1901                continue
1902            elif (
1903                isinstance(brailleElement, bar.Barline)
1904                and elementsInCurrentSegment > MAX_ELEMENTS_IN_SEGMENT
1905                and brailleElement.type in ('double', 'final')
1906            ):
1907                # see test_drill10_2
1908                startANewSegment = True
1909                # execute the block below to ensure barline is added to current segment
1910
1911            if brailleElement.affinityCode < previousAffinityCode:
1912                ordinal += 1
1913
1914            affinityCode = brailleElement.affinityCode
1915            if affinityCode == Affinity.SPLIT1_NOTEGROUP:
1916                affinityCode = Affinity.INACCORD
1917            elif affinityCode == Affinity.SPLIT2_NOTEGROUP:
1918                affinityCode = Affinity.NOTEGROUP
1919
1920            segmentKey = SegmentKey(music21Measure.number,
1921                                    ordinal,
1922                                    affinityCode,
1923                                    setHand
1924                                    )
1925            if segmentKey not in currentSegment:
1926                currentSegment[segmentKey] = BrailleElementGrouping()
1927            brailleElementGrouping = currentSegment[segmentKey]
1928            brailleElementGrouping.append(brailleElement)
1929            elementsInCurrentSegment += 1
1930
1931            # NOT variable affinityCode!
1932            previousAffinityCode = brailleElement.affinityCode
1933    allSegments.append(currentSegment)
1934    return allSegments
1935
1936
1937def extractBrailleElements(music21Measure):
1938    '''
1939    Takes in a :class:`~music21.stream.Measure` and returns a
1940    :class:`~music21.braille.segment.BrailleElementGrouping` of correctly ordered
1941    :class:`~music21.base.Music21Object` instances which can be directly transcribed to
1942    braille.
1943
1944    >>> from music21.braille import segment
1945    >>> tn = converter.parse('tinynotation: 2/4 c16 c c c d d d d', makeNotation=False)
1946    >>> tn = tn.makeNotation(cautionaryNotImmediateRepeat=False)
1947    >>> measure = tn[0]
1948    >>> measure.append(spanner.Slur(measure.notes[0],measure.notes[-1]))
1949    >>> measure.show('text')
1950    {0.0} <music21.clef.TrebleClef>
1951    {0.0} <music21.meter.TimeSignature 2/4>
1952    {0.0} <music21.note.Note C>
1953    {0.25} <music21.note.Note C>
1954    {0.5} <music21.note.Note C>
1955    {0.75} <music21.note.Note C>
1956    {1.0} <music21.note.Note D>
1957    {1.25} <music21.note.Note D>
1958    {1.5} <music21.note.Note D>
1959    {1.75} <music21.note.Note D>
1960    {2.0} <music21.spanner.Slur <music21.note.Note C><music21.note.Note D>>
1961    {2.0} <music21.bar.Barline type=final>
1962
1963
1964    Spanners are dealt with in :func:`~music21.braille.segment.prepareSlurredNotes`,
1965    so they are not returned by this function, as seen below.
1966
1967    >>> print(segment.extractBrailleElements(measure))
1968    <music21.meter.TimeSignature 2/4>
1969    <music21.clef.TrebleClef>
1970    <music21.note.Note C>
1971    <music21.note.Note C>
1972    <music21.note.Note C>
1973    <music21.note.Note C>
1974    <music21.note.Note D>
1975    <music21.note.Note D>
1976    <music21.note.Note D>
1977    <music21.note.Note D>
1978    <music21.bar.Barline type=final>
1979    '''
1980    allElements = BrailleElementGrouping()
1981    for music21Object in music21Measure:
1982        try:
1983            if isinstance(music21Object, bar.Barline):
1984                if music21Object.type == 'regular':
1985                    continue
1986            setAffinityCode(music21Object)
1987            music21Object.editorial.brailleEnglish = [str(music21Object)]
1988            allElements.append(music21Object)
1989        except BrailleSegmentException as notSupportedException:  # pragma: no cover
1990            isExempt = [isinstance(music21Object, music21Class)
1991                        for music21Class in excludeFromBrailleElements]
1992            if isExempt.count(True) == 0:
1993                environRules.warn(f'{notSupportedException}')
1994
1995    allElements.sort(key=lambda x: (x.offset, x.classSortOrder))
1996    if len(allElements) >= 2 and isinstance(allElements[-1], dynamics.Dynamic):
1997        if isinstance(allElements[-2], bar.Barline):
1998            allElements[-1].classSortOrder = -1
1999            allElements.sort(key=lambda x: (x.offset, x.classSortOrder))
2000
2001    return allElements
2002
2003
2004def prepareBeamedNotes(music21Measure):
2005    '''
2006    Takes in a :class:`~music21.stream.Measure` and labels beamed notes
2007    of smaller value than an 8th with beamStart and beamContinue keywords
2008    in accordance with beaming rules in braille music.
2009
2010    A more in-depth explanation of beaming in braille can be found in
2011    Chapter 15 of Introduction to Braille Music Transcription, Second
2012    Edition, by Mary Turner De Garmo.
2013
2014    >>> from music21.braille import segment
2015    >>> tn = converter.parse('tinynotation: 2/4 c16 c c c d d d d')
2016    >>> tn = tn.makeNotation(cautionaryNotImmediateRepeat=False)
2017    >>> tn.show('text')
2018    {0.0} <music21.stream.Measure 1 offset=0.0>
2019        {0.0} <music21.clef.TrebleClef>
2020        {0.0} <music21.meter.TimeSignature 2/4>
2021        {0.0} <music21.note.Note C>
2022        {0.25} <music21.note.Note C>
2023        {0.5} <music21.note.Note C>
2024        {0.75} <music21.note.Note C>
2025        {1.0} <music21.note.Note D>
2026        {1.25} <music21.note.Note D>
2027        {1.5} <music21.note.Note D>
2028        {1.75} <music21.note.Note D>
2029        {2.0} <music21.bar.Barline type=final>
2030    >>> measure = tn[0]
2031    >>> segment.prepareBeamedNotes(measure)
2032    >>> measure.notes[0].beamStart
2033    True
2034    >>> measure.notes[1].beamContinue
2035    True
2036    >>> measure.notes[2].beamContinue
2037    True
2038    >>> measure.notes[3].beamContinue
2039    True
2040    '''
2041    allNotes = music21Measure.notes.stream()
2042
2043    for sampleNote in allNotes:
2044        sampleNote.beamStart = False
2045        sampleNote.beamContinue = False
2046    allNotesAndRests = music21Measure.notesAndRests.stream()
2047
2048    def withBeamFilter(el, unused):
2049        return (el.beams is not None) and len(el.beams) > 0
2050
2051    def beamStartFilter(el, unused):
2052        return el.beams.getByNumber(1).type == 'start'
2053
2054    def beamStopFilter(el, unused):
2055        return el.beams.getByNumber(1).type == 'stop'
2056
2057    allStartIter = allNotes.iter().addFilter(withBeamFilter).addFilter(beamStartFilter)
2058    allStopIter = allNotes.iter().addFilter(withBeamFilter).addFilter(beamStopFilter)
2059
2060    if len(allStartIter) != len(allStopIter):
2061        environRules.warn('Incorrect beaming: number of start notes != to number of stop notes.')
2062        return
2063
2064    for beamIndex, startNote in enumerate(allStartIter):
2065        # Eighth notes cannot be beamed in braille (redundant, because beamed
2066        # notes look like eighth notes, but nevertheless useful).
2067        if startNote.quarterLength == 0.5:
2068            continue
2069
2070        stopNote = allStopIter[beamIndex]
2071        startIndex = allNotesAndRests.index(startNote)
2072        stopIndex = allNotesAndRests.index(stopNote)
2073
2074        delta = stopIndex - startIndex + 1
2075        if delta < 3:  # 2. The group must be composed of at least three notes.
2076            continue
2077        # 1. All notes in the group must have precisely the same value.
2078        # 3. A rest of the same value may take the place of the first note in a group,
2079        # but if the rest is located anywhere else, grouping may not be used.
2080        allNotesOfSameValue = True
2081        for noteIndex in range(startIndex + 1, stopIndex + 1):
2082            if (allNotesAndRests[noteIndex].quarterLength != startNote.quarterLength
2083                    or isinstance(allNotesAndRests[noteIndex], note.Rest)):
2084                allNotesOfSameValue = False
2085                break
2086        try:
2087            afterStopNote = allNotesAndRests[stopIndex + 1]
2088            if (isinstance(afterStopNote, note.Rest)
2089                    and (int(afterStopNote.beat) == int(stopNote.beat))):
2090                allNotesOfSameValue = False
2091        except IndexError:  # stopNote is last note of measure.
2092            pass
2093        if not allNotesOfSameValue:
2094            continue
2095        try:
2096            # 4. If the notes in the group are followed immediately by a
2097            # true eighth note or by an eighth rest,
2098            # grouping may not be used, unless the eighth is located in a new measure.
2099            if allNotesAndRests[stopIndex + 1].quarterLength == 0.5:
2100                continue
2101        except IndexError:  # stopNote is last note of measure.
2102            pass
2103
2104        startNote.beamStart = True
2105        try:
2106            beforeStartNote = allNotesAndRests[startIndex - 1]
2107            if (isinstance(beforeStartNote, note.Rest)
2108                    and int(beforeStartNote.beat) == int(startNote.beat)
2109                    and beforeStartNote.quarterLength == startNote.quarterLength):
2110                startNote.beamContinue = True
2111        except IndexError:  # startNote is first note of measure.
2112            pass
2113        for noteIndex in range(startIndex + 1, stopIndex + 1):
2114            allNotesAndRests[noteIndex].beamContinue = True
2115
2116
2117def setAffinityCode(music21Object):
2118    '''
2119    Takes in a :class:`~music21.base.Music21Object`, and does two things:
2120
2121    * Modifies the :attr:`~music21.base.Music21Object.classSortOrder` attribute of the
2122      object to fit the slightly modified ordering of objects in braille music.
2123
2124    * Adds an affinity code to the object. This code indicates which surrounding
2125      objects the object should be grouped with.
2126
2127
2128    A BrailleSegmentException is raised if an affinity code cannot be assigned to
2129    the object.
2130
2131
2132    As seen in the following example, the affinity code of a :class:`~music21.note.Note`
2133    and a :class:`~music21.clef.TrebleClef` are the same, because they should be grouped
2134    together. However, the classSortOrder indicates that the TrebleClef should come first
2135    in the braille.
2136
2137    >>> n1 = note.Note('D5')
2138    >>> braille.segment.setAffinityCode(n1)
2139    >>> n1.affinityCode
2140    <Affinity.NOTEGROUP: 9>
2141    >>> n1.classSortOrder
2142    10
2143    >>> c1 = clef.TrebleClef()
2144    >>> braille.segment.setAffinityCode(c1)
2145    >>> c1.affinityCode
2146    <Affinity.NOTEGROUP: 9>
2147    >>> c1.classSortOrder
2148    7
2149    '''
2150    for (music21Class, code, sortOrder) in affinityCodes:
2151        if isinstance(music21Object, music21Class):
2152            music21Object.affinityCode = code
2153            music21Object.classSortOrder = sortOrder
2154            return
2155
2156    if isinstance(music21Object, expressions.TextExpression):
2157        music21Object.affinityCode = Affinity.NOTEGROUP
2158        if len(music21Object.content.split()) > 1:
2159            music21Object.affinityCode = Affinity.LONG_TEXTEXPR
2160        music21Object.classSortOrder = 8
2161        return
2162
2163    if isinstance(music21Object, BrailleTranscriptionHelper):
2164        return
2165
2166    raise BrailleSegmentException(f'{music21Object} cannot be transcribed to braille.')
2167
2168
2169def areGroupingsIdentical(noteGroupingA, noteGroupingB):
2170    '''
2171    Takes in two note groupings, noteGroupingA and noteGroupingB. Returns True
2172    if both groupings have identical contents. False otherwise.
2173
2174    Helper for numRepeats...
2175
2176    Needs two identical length groupings.
2177
2178    >>> a = [note.Note('C4'), note.Note('D4')]
2179    >>> b = [note.Note('C4'), note.Note('D4')]
2180    >>> braille.segment.areGroupingsIdentical(a, b)
2181    True
2182
2183    >>> d = b.pop()
2184    >>> braille.segment.areGroupingsIdentical(a, b)
2185    False
2186    >>> c = [note.Rest(), note.Note('D4')]
2187    >>> braille.segment.areGroupingsIdentical(a, c)
2188    False
2189    '''
2190    if len(noteGroupingA) == len(noteGroupingB):
2191        for (elementA, elementB) in zip(noteGroupingA, noteGroupingB):
2192            if elementA != elementB:
2193                return False
2194        return True
2195    return False
2196
2197
2198# ------------------------------------------------------------------------------
2199# Helper Methods
2200
2201def splitNoteGrouping(noteGrouping, beatDivisionOffset=0):
2202    '''
2203    Almost identical to :func:`~music21.braille.segment.splitMeasure`, but
2204    functions on a :class:`~music21.braille.segment.BrailleElementGrouping`
2205    instead.
2206
2207    >>> from music21.braille import segment
2208    >>> bg = segment.BrailleElementGrouping()
2209    >>> bg.timeSignature = meter.TimeSignature('2/2')
2210    >>> s = converter.parse('tinyNotation: 2/2 c4 d r e')
2211    >>> for n in s.recurse().notesAndRests:
2212    ...     bg.append(n)
2213    >>> left, right = segment.splitNoteGrouping(bg)
2214    >>> left
2215    <music21.braille.segment.BrailleElementGrouping
2216        [<music21.note.Note C>, <music21.note.Note D>]>
2217
2218    >>> print(left)
2219    <music21.note.Note C>
2220    <music21.note.Note D>
2221
2222    >>> right
2223    <music21.braille.segment.BrailleElementGrouping
2224        [<music21.note.Rest quarter>, <music21.note.Note E>]>
2225
2226
2227    Now split one beat division earlier than it should be.  For 2/2 that means
2228    one half of a beat, or one quarter note earlier:
2229
2230    >>> left, right = segment.splitNoteGrouping(bg, beatDivisionOffset=1)
2231    >>> left
2232    <music21.braille.segment.BrailleElementGrouping
2233        [<music21.note.Note C>]>
2234    >>> right
2235    <music21.braille.segment.BrailleElementGrouping
2236        [<music21.note.Note D>, <music21.note.Rest quarter>, <music21.note.Note E>]>
2237    '''
2238    music21Measure = stream.Measure()
2239    for brailleElement in noteGrouping:
2240        music21Measure.insert(brailleElement.offset, brailleElement)
2241    (leftMeasure, rightMeasure) = splitMeasure(music21Measure,
2242                                               beatDivisionOffset,
2243                                               noteGrouping.timeSignature)
2244    leftBrailleElements = copy.copy(noteGrouping)
2245    leftBrailleElements.internalList = []
2246    for brailleElement in leftMeasure:
2247        leftBrailleElements.append(brailleElement)
2248
2249    rightBrailleElements = copy.copy(noteGrouping)
2250    rightBrailleElements.internalList = []
2251    for brailleElement in rightMeasure:
2252        rightBrailleElements.append(brailleElement)
2253
2254    return leftBrailleElements, rightBrailleElements
2255
2256
2257def splitMeasure(music21Measure, beatDivisionOffset=0, useTimeSignature=None):
2258    '''
2259    Takes a :class:`~music21.stream.Measure`, divides it in two parts, and returns a
2260    two-tuple of (leftMeasure, rightMeasure). The parameters are as
2261    follows:
2262
2263    * beatDivisionOffset => Adjusts the end offset of the first partition by a certain amount
2264      of beats to the left.
2265    * useTimeSignature => In the event that the Measure comes from the middle of a Part
2266      and thus does not define an explicit :class:`~music21.meter.TimeSignature`. If not
2267      provided, a TimeSignature is retrieved by
2268      using :meth:`~music21.stream.Measure.bestTimeSignature`.
2269
2270    >>> m = stream.Measure()
2271    >>> m.append(note.Note('C4'))
2272    >>> m.append(note.Note('D4'))
2273    >>> left, right = braille.segment.splitMeasure(m)
2274    >>> left.show('text')
2275    {0.0} <music21.note.Note C>
2276    >>> right.show('text')
2277    {1.0} <music21.note.Note D>
2278    '''
2279    if useTimeSignature is not None:
2280        ts = useTimeSignature
2281    else:
2282        ts = music21Measure.bestTimeSignature()
2283
2284    offset = 0.0
2285    if beatDivisionOffset != 0:
2286        if abs(beatDivisionOffset) > len(ts.beatDivisionDurations):
2287            raise BrailleSegmentException(
2288                f'beatDivisionOffset {beatDivisionOffset} is outside '
2289                + f'of ts.beatDivisionDurations {ts.beatDivisionDurations}'
2290            )
2291        duration_index = len(ts.beatDivisionDurations) - abs(beatDivisionOffset)
2292        try:
2293            offset += opFrac(ts.beatDivisionDurations[duration_index].quarterLength)
2294            offset = opFrac(offset)
2295        except IndexError:
2296            environRules.warn('Problem in converting a time signature in measure '
2297                              + f'{music21Measure.number}, offset may be wrong')
2298    bs = copy.deepcopy(ts.beatSequence)
2299
2300    numberOfPartitions = 2
2301    try:
2302        bs.partitionByCount(numberOfPartitions, loadDefault=False)
2303        (startOffsetZero, endOffsetZero) = bs.getLevelSpan()[0]
2304    except meter.MeterException:
2305        numberOfPartitions += 1
2306        bs.partitionByCount(numberOfPartitions, loadDefault=False)
2307        startOffsetZero = bs.getLevelSpan()[0][0]
2308        endOffsetZero = bs.getLevelSpan()[-2][-1]
2309    endOffsetZero -= offset
2310
2311    leftMeasure = stream.Measure()
2312    rightMeasure = stream.Measure()
2313    for x in music21Measure:
2314        if (x.offset >= startOffsetZero
2315                and (x.offset < endOffsetZero
2316                     or (x.offset == endOffsetZero
2317                         and isinstance(x, bar.Barline)))):
2318            leftMeasure.insert(x.offset, x)
2319        else:
2320            rightMeasure.insert(x.offset, x)
2321    for n in rightMeasure.notes:
2322        if n.tie is not None:
2323            leftMeasure.append(n)
2324            rightMeasure.remove(n)
2325            endOffsetZero += n.duration.quarterLength
2326            continue
2327        break
2328
2329    rest0Length = music21Measure.duration.quarterLength - endOffsetZero
2330    r0 = note.Rest(quarterLength=rest0Length)
2331    leftMeasure.insert(endOffsetZero, r0)
2332
2333    r1 = note.Rest(quarterLength=endOffsetZero)
2334    rightMeasure.insert(0.0, r1)
2335
2336    ts0_delete = False
2337    if leftMeasure.timeSignature is None:
2338        ts0_delete = True
2339        leftMeasure.timeSignature = ts
2340    rightMeasure.timeSignature = ts
2341    leftMeasure.mergeAttributes(music21Measure)
2342    rightMeasure.mergeAttributes(music21Measure)
2343    leftMeasure.makeBeams(inPlace=True)
2344    rightMeasure.makeBeams(inPlace=True)
2345    prepareBeamedNotes(leftMeasure)
2346    prepareBeamedNotes(rightMeasure)
2347    leftMeasure.remove(r0)
2348    rightMeasure.remove(r1)
2349    if ts0_delete:
2350        leftMeasure.remove(ts)
2351    rightMeasure.remove(ts)
2352    return (leftMeasure, rightMeasure)
2353
2354
2355# ------------------------------------------------------------------------------
2356
2357
2358class Test(unittest.TestCase):
2359
2360    def testGetRawSegments(self):
2361        from music21 import converter
2362
2363        tn = converter.parse("tinynotation: 3/4 c4 c c e e e g g g c'2.")
2364        tn = tn.makeNotation(cautionaryNotImmediateRepeat=False)
2365
2366        rawSegList = getRawSegments(tn)
2367        unused = str(rawSegList[0])
2368
2369
2370if __name__ == '__main__':
2371    import music21
2372    music21.mainTest(Test)  # , runTest='testGetRawSegments')
2373
2374