# ------------------------------------------------------------------------------
# Name: text.py
# Purpose: music21 class which allows for accurate formatting of braille transcription
# Authors: Jose Cabal-Ugaz
# Michael Scott Asato Cuthbert
#
# Copyright: Copyright © 2011, 2016 Michael Scott Asato Cuthbert
# License: BSD, see license.txt
# ------------------------------------------------------------------------------
from __future__ import annotations
import unittest
from music21 import exceptions21
from music21 import prebase
from music21.braille import lookup
from music21.braille.basic import numberToBraille, yieldDots
symbols = lookup.symbols
[docs]
class BrailleText(prebase.ProtoM21Object):
'''
Object that handles all the formatting associated with braille music notation on multiple lines.
>>> bt = braille.text.BrailleText(lineLength=10, showHand='right')
>>> bt
<music21.braille.text.BrailleText 1 line, 0 headings, 10 cols>
>>> bt.lineLength
10
>>> bt.allLines
[<music21.braille.text.BrailleTextLine '⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀'>]
>>> bt.rightHandSymbol
True
>>> bt.leftHandSymbol
False
>>> bt.allHeadings
[]
'''
def __init__(self, lineLength=40, showHand=None):
self.lineLength = lineLength
self.currentLine = None
self.allLines = []
self.makeNewLine()
self._showHand = None
self.rightHandSymbol = False
self.leftHandSymbol = False
self.allHeadings = []
self.showHand = showHand
def _reprInternal(self) -> str:
line_len = len(self.allLines)
line_str = f'{line_len} line' + ('s' if line_len != 1 else '')
heading_len = len(self.allHeadings)
heading_str = f'{heading_len} heading' + ('s' if heading_len != 1 else '')
return f'{line_str}, {heading_str}, {self.lineLength} cols'
@property
def showHand(self):
return self._showHand
@showHand.setter
def showHand(self, newHand):
if newHand == 'right':
self.rightHandSymbol = True
elif newHand == 'left':
self.leftHandSymbol = True
elif newHand is not None:
raise BrailleTextException('Illegal hand sign request.')
[docs]
def addHeading(self, heading):
'''
adds a heading to the BrailleText. Heading can be a single or multiple line
Unicode string representing a heading.
These headings are not stored in allHeadings, but instead in .allLines,
what .allHeadings stores is the index of the start of a heading section
and the index of the end of a heading section.
(since each BrailleTextLine knows whether it is a heading or not, storing
the index of headings might be overkill)
>>> bt = braille.text.BrailleText(lineLength=10)
>>> headingText = braille.basic.timeSigToBraille(meter.TimeSignature('4/8'))
>>> bt.addHeading(headingText)
>>> len(bt.allLines)
2
>>> bt.allLines[0].isHeading
True
>>> print(str(bt.allLines[0]))
⠼⠙⠦
>>> bt.allHeadings
[(0, 1)]
>>> bt.addMeasureNumber(7)
>>> headingText = braille.basic.timeSigToBraille(meter.TimeSignature('3/4'))
>>> bt.addHeading(headingText)
>>> len(bt.allLines)
4
>>> bt.allHeadings
[(0, 1), (2, 3)]
'''
if self.currentLine.textLocation != 0:
self.makeNewLine()
indexStart = len(self.allLines) - 1
indexFinal = indexStart
brailleCurrentLine = self.currentLine
for headingLine in heading.splitlines():
brailleCurrentLine.isHeading = True
brailleCurrentLine.append(headingLine, addSpace=False)
self.makeNewLine()
indexFinal += 1
self.allHeadings.append((indexStart, indexFinal))
[docs]
def addLongExpression(self, longExpr):
'''
Adds an expression long enough that it is split at
each space symbol such that line wrapping could occur.
'''
for brailleExpr in longExpr.split(symbols['space']):
self.appendOrInsertCurrent(brailleExpr)
[docs]
def addToNewLine(self, brailleNoteGrouping):
'''
Adds a NoteGrouping to a new line, prefacing that new line
with the appropriate spaces or keyboard symbols and dots.
>>> bt = braille.text.BrailleText(10)
>>> bt.currentLine.append('hi', addSpace=False)
>>> print(str(bt))
hi
>>> c = braille.lookup.pitchNameToNotes['C']['quarter'] # dots 1456
>>> bt.addToNewLine(c + c + c)
>>> print(str(bt))
hi
⠀⠀⠹⠹⠹
It is done differently if there are hand symbols involved:
>>> bt = braille.text.BrailleText(10)
>>> bt.showHand = 'right'
>>> bt.currentLine.append('hi', addSpace=False)
>>> bt.addToNewLine(c + c + c)
>>> print(str(bt))
hi
⠨⠜⠄⠹⠹⠹
'''
self.makeNewLine()
if self.rightHandSymbol or self.leftHandSymbol:
self.optionalAddKeyboardSymbolsAndDots(brailleNoteGrouping)
self.currentLine.append(brailleNoteGrouping, addSpace=False)
else:
self.currentLine.insert(2, brailleNoteGrouping)
[docs]
def appendOrInsertCurrent(self, brailleExpr, addSpace=True):
'''
append expression to the current line if it is possible,
or make a new line and insert it there:
>>> bt = braille.text.BrailleText(lineLength=10)
>>> bt.appendOrInsertCurrent('hello', addSpace=False)
>>> print(str(bt))
hello
>>> bt.appendOrInsertCurrent(braille.lookup.symbols['space'] + 'hi')
>>> print(str(bt))
hello⠀⠀hi
>>> bt.appendOrInsertCurrent(braille.lookup.symbols['space'] + 'there')
>>> print(str(bt))
hello⠀⠀hi
⠀⠀⠀there
'''
if self.currentLine.canAppend(brailleExpr, addSpace=addSpace):
self.currentLine.append(brailleExpr, addSpace=addSpace)
else:
self.makeNewLine()
self.currentLine.insert(2, brailleExpr)
[docs]
def addInaccord(self, inaccord):
addSpace = self.optionalAddKeyboardSymbolsAndDots(inaccord)
try:
self.currentLine.append(inaccord, addSpace=addSpace)
except BrailleTextException:
self.makeNewLine()
if self.rightHandSymbol or self.leftHandSymbol:
if self.rightHandSymbol:
self.currentLine.insert(2, symbols['rh_keyboard'])
elif self.leftHandSymbol:
self.currentLine.insert(2, symbols['lh_keyboard'])
for dot in yieldDots(inaccord[0]):
self.currentLine.append(dot, addSpace=False)
self.currentLine.append(inaccord, addSpace=False)
else:
self.currentLine.insert(2, inaccord)
self.currentLine.containsNoteGrouping = True
[docs]
def addMeasureNumber(self, measureNumber):
'''
Add a measure number (either a braille number or an int).
>>> bt = braille.text.BrailleText(lineLength=10)
>>> bt
<music21.braille.text.BrailleText 1 line, 0 headings, 10 cols>
>>> bt.allLines
[<music21.braille.text.BrailleTextLine '⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀'>]
>>> bt.addMeasureNumber(4)
>>> print(str(bt.allLines[0]))
⠼⠙
>>> bt.currentLine.textLocation
2
If there are already lines, then add a new one:
>>> bt.addMeasureNumber(5)
>>> bt.allLines
[<music21.braille.text.BrailleTextLine '⠼⠙⠀⠀⠀⠀⠀⠀⠀⠀'>,
<music21.braille.text.BrailleTextLine '⠼⠑⠀⠀⠀⠀⠀⠀⠀⠀'>]
>>> print(str(bt.allLines[-1]))
⠼⠑
'''
if isinstance(measureNumber, int):
measureNumber = numberToBraille(measureNumber)
if self.currentLine.textLocation != 0:
self.makeNewLine()
self.currentLine.append(measureNumber, addSpace=False)
[docs]
def optionalAddKeyboardSymbolsAndDots(self, noteGrouping=None):
'''
Adds symbols for rh_keyboard or lh_keyboard depending on what
is appropriate
returns a boolean indicating whether a space needs to be added
before the next symbol is needed.
'''
addSpace = True
if (not self.currentLine.containsNoteGrouping
and (self.rightHandSymbol or self.leftHandSymbol)):
if self.currentLine.textLocation == 0:
addSpace = False
if self.rightHandSymbol:
self.currentLine.append(symbols['rh_keyboard'], addSpace=addSpace)
elif self.leftHandSymbol:
self.currentLine.append(symbols['lh_keyboard'], addSpace=addSpace)
if noteGrouping:
for dot in yieldDots(noteGrouping[0]):
self.currentLine.append(dot, addSpace=False)
addSpace = False
if self.currentLine.textLocation == 0:
addSpace = False
return addSpace
[docs]
def addSignatures(self, signatures):
'''
Appends signatures to the current location if there is space, otherwise appends to
a new line:
>>> bt = braille.text.BrailleText(lineLength=5)
>>> bt.addSignatures(braille.basic.timeSigToBraille(meter.TimeSignature('4/8')))
>>> print(str(bt.currentLine))
⠼⠙⠦
>>> bt.addSignatures(braille.basic.timeSigToBraille(meter.TimeSignature('3/4')))
>>> print(str(bt.currentLine))
⠀⠀⠼⠉⠲
>>> len(bt.allLines)
2
'''
addSpace = True
if self.currentLine.textLocation == 0:
addSpace = False
self.appendOrInsertCurrent(signatures, addSpace=addSpace)
[docs]
def makeNewLine(self):
'''
Add a newline to the BrailleText
>>> bt = braille.text.BrailleText(lineLength=10)
>>> len(bt.allLines)
1
>>> bt.makeNewLine()
>>> len(bt.allLines)
2
>>> bt.makeNewLine()
>>> len(bt.allLines)
3
'''
self.currentLine = BrailleTextLine(self.lineLength)
self.allLines.append(self.currentLine)
[docs]
def recenterHeadings(self):
'''
Recenter each of the headings so that they exactly align
with the text beneath them.
Demonstration with non braille text:
>>> heading1 = 'hello'
>>> body1 = 'anyoneHome?' + braille.lookup.symbols['space'] + 'yup!'
>>> bt = braille.text.BrailleText(lineLength=12)
>>> bt.addHeading(heading1)
>>> bt.addLongExpression(body1)
>>> bt.allHeadings
[(0, 1)]
>>> bt.recenterHeadings()
>>> print(str(bt))
⠀⠀⠀hello⠀⠀⠀⠀
⠀anyoneHome?
⠀⠀yup!
Each heading is aligned with its own text
>>> heading2 = 'buh'
>>> body2 = 'short' + braille.lookup.symbols['space'] + 'court'
>>> bt.addHeading(heading2)
>>> bt.addLongExpression(body2)
>>> bt.allHeadings
[(0, 1), (3, 4)]
>>> bt.recenterHeadings()
>>> print(str(bt))
⠀⠀⠀hello⠀⠀⠀⠀
⠀anyoneHome?
⠀⠀yup!
⠀⠀⠀⠀buh⠀⠀⠀⠀⠀
⠀short⠀court
'''
for (indexStart, indexFinal) in self.allHeadings:
maxLineLength = 0 # refers to the maximum length of any heading.
for i in range(indexFinal, len(self.allLines)):
if self.allLines[i].isHeading:
break
lineLength = self.allLines[i].textLocation
maxLineLength = max(maxLineLength, lineLength)
for j in range(indexStart, indexFinal):
brailleTextLine = self.allLines[j]
lineStrToCenter = str(brailleTextLine)
lineStrToCenter = lineStrToCenter.strip(symbols['space'])
if maxLineLength > len(lineStrToCenter):
lineStrToCenter = lineStrToCenter.center(maxLineLength, symbols['space'])
brailleTextLine.insert(0, lineStrToCenter)
brailleTextLine.textLocation = maxLineLength
def __str__(self):
self.recenterHeadings()
return '\n'.join([str(line) for line in self.allLines])
[docs]
class BrailleKeyboard(BrailleText):
'''
A subclass of BrailleText that handles both hands at once.
'''
def __init__(self, lineLength=40):
super().__init__(lineLength=lineLength)
self.rightHandLine = None
self.leftHandLine = None
self.highestMeasureNumberLength = 0 # used in BrailleKeyboard layouts
# def addElement(self, **elementKeywords):
# if 'pair' in elementKeywords:
# (measureNumber, noteGroupingR, noteGroupingL) = elementKeywords['pair']
# self.addNoteGroupings(measureNumber, noteGroupingL, noteGroupingR)
# else:
# return super().addElement(**elementKeywords)
[docs]
def makeNewLines(self):
if self.currentLine.textLocation == 0:
self.rightHandLine = self.currentLine
else:
self.rightHandLine = BrailleTextLine(self.lineLength)
self.allLines.append(self.rightHandLine)
self.leftHandLine = BrailleTextLine(self.lineLength)
self.allLines.append(self.leftHandLine)
[docs]
def addNoteGroupings(self, measureNumber, noteGroupingR, noteGroupingL):
if self.rightHandLine is None and self.leftHandLine is None:
self.makeNewLines()
if self.rightHandLine.textLocation == 0:
self.rightHandLine.insert(self.highestMeasureNumberLength - len(measureNumber),
measureNumber)
self.leftHandLine.textLocation = self.rightHandLine.textLocation
addSpace = True
if self.rightHandLine.containsNoteGrouping is False:
addSpace = False
self.rightHandLine.append(symbols['rh_keyboard'], addSpace=True)
self.leftHandLine.append(symbols['lh_keyboard'], addSpace=True)
if noteGroupingR:
for dot in yieldDots(noteGroupingR[0]):
self.rightHandLine.append(dot, addSpace=False)
if noteGroupingL:
for dot in yieldDots(noteGroupingL[0]):
self.leftHandLine.append(dot, addSpace=False)
if (self.rightHandLine.canAppend(noteGroupingR, addSpace=addSpace)
and self.leftHandLine.canAppend(noteGroupingL, addSpace=addSpace)):
if noteGroupingL:
self.leftHandLine.append(noteGroupingL, addSpace=addSpace)
if noteGroupingR:
self.rightHandLine.append(noteGroupingR, addSpace=addSpace)
if self.rightHandLine.textLocation > self.leftHandLine.textLocation:
self.leftHandLine.textLocation = self.rightHandLine.textLocation
else:
self.rightHandLine.textLocation = self.leftHandLine.textLocation
else:
self.makeNewLines()
self.rightHandLine.insert(self.highestMeasureNumberLength - len(measureNumber),
measureNumber)
self.leftHandLine.textLocation = self.rightHandLine.textLocation
self.rightHandLine.append(symbols['rh_keyboard'], addSpace=True)
self.leftHandLine.append(symbols['lh_keyboard'], addSpace=True)
if noteGroupingR:
for dot in yieldDots(noteGroupingR[0]):
self.rightHandLine.append(dot, addSpace=False)
self.rightHandLine.append(noteGroupingR, addSpace=False)
if noteGroupingL:
for dot in yieldDots(noteGroupingL[0]):
self.leftHandLine.append(dot, addSpace=False)
self.leftHandLine.append(noteGroupingL, addSpace=False)
if self.rightHandLine.textLocation > self.leftHandLine.textLocation:
self.leftHandLine.textLocation = self.rightHandLine.textLocation
else:
self.rightHandLine.textLocation = self.leftHandLine.textLocation
self.rightHandLine.containsNoteGrouping = True
self.leftHandLine.containsNoteGrouping = True
[docs]
class BrailleTextLine(prebase.ProtoM21Object):
'''
An object representing a single line of braille text:
The initial value is the length of the line:
>>> btl = braille.text.BrailleTextLine(40)
>>> btl
<music21.braille.text.BrailleTextLine '⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀'>
>>> btl.isHeading
False
>>> btl.containsNoteGrouping
False
>>> btl.lineLength
40
>>> btl.textLocation
0
>>> btl.highestUsedLocation
0
>>> btl.allChars == 40 * [braille.lookup.symbols['space']]
True
>>> btl.append(braille.lookup.symbols['tie'])
>>> btl
<music21.braille.text.BrailleTextLine '⠀⠈⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀'>
>>> print(str(btl))
⠀⠈⠉
'''
def __init__(self, lineLength: int = 40):
self.isHeading = False
self.containsNoteGrouping = False
self.lineLength = lineLength
self.allChars = self.lineLength * [symbols['space']]
self.textLocation = 0
self.highestUsedLocation = 0
def _reprInternal(self) -> str:
return repr(''. join(self.allChars))
[docs]
def append(self, text, addSpace=True):
'''
Appends text (with optional space at the beginning) or raises an
exception if it cannot be appended.
>>> btl = braille.text.BrailleTextLine(6)
>>> btl.append(braille.lookup.symbols['tie'], addSpace=False)
>>> print(str(btl))
⠈⠉
>>> btl.textLocation
2
>>> btl.highestUsedLocation
2
Default is to add a space:
>>> btl.append(braille.lookup.symbols['tie'])
>>> print(str(btl))
⠈⠉⠀⠈⠉
Out of room:
>>> btl.append(braille.lookup.symbols['tie'])
Traceback (most recent call last):
music21.braille.text.BrailleTextException: Text does not fit at end of braille text line.
Text is appended at `textLocation`, overwriting other text that might be there.
>>> btl.textLocation = btl.highestUsedLocation = 0
>>> btl.append('hi', addSpace=False)
>>> btl.textLocation = btl.highestUsedLocation = 5
>>> print(str(btl))
hi⠀⠈⠉
'''
if not self.canAppend(text, addSpace):
raise BrailleTextException('Text does not fit at end of braille text line.')
if addSpace:
self.allChars[self.textLocation] = symbols['space']
self.textLocation += 1
for char in list(text):
self.allChars[self.textLocation] = char
self.textLocation += 1
self.highestUsedLocation = self.textLocation
[docs]
def insert(self, textLocation, text):
'''
Inserts text at a certain location, updating textLocation and possibly
highestUsedLocation:
>>> btl = braille.text.BrailleTextLine(6)
>>> btl.insert(2, braille.lookup.symbols['tie'])
>>> print(str(btl))
⠀⠀⠈⠉
>>> btl.textLocation
4
>>> btl.highestUsedLocation
4
>>> btl.insert(0, braille.lookup.symbols['tie'])
It looks like we have deleted the previous tie:
>>> print(str(btl))
⠈⠉
But that's because only characters up to .textLocation are printed
(this may change later)
>>> btl.textLocation
2
>>> btl.highestUsedLocation
4
Let's change textLocation and now see:
>>> btl.textLocation = btl.highestUsedLocation
>>> print(str(btl))
⠈⠉⠈⠉
Inserting beyond the end creates an error:
>>> btl.insert(5, braille.lookup.symbols['tie'])
Traceback (most recent call last):
music21.braille.text.BrailleTextException: Text cannot be inserted at specified location.
Unlike list inserts, this insert overwrites the previous text:
>>> btl.insert(0, 'hi')
>>> btl.textLocation = btl.highestUsedLocation
>>> print(str(btl))
hi⠈⠉
'''
if not self.canInsert(textLocation, text):
raise BrailleTextException('Text cannot be inserted at specified location.')
for i, char in enumerate(text, start=textLocation):
self.allChars[i] = char
self.textLocation = textLocation + len(text)
self.highestUsedLocation = max(self.highestUsedLocation, self.textLocation)
[docs]
def canAppend(self, text, addSpace=True):
'''
Returns True if there is enough space in this line to append the text, or False
if not:
>>> btl = braille.text.BrailleTextLine(10)
>>> btl.canAppend('1234567890', addSpace=False)
True
>>> btl.canAppend('12345678901', addSpace=False)
False
>>> btl.canAppend('1234567890', addSpace=True)
False
>>> btl.textLocation
0
>>> btl.textLocation = 5
>>> btl.canAppend('12345', addSpace=False)
True
>>> btl.canAppend('123456', addSpace=False)
False
If highestUsedLocation > textLocation, highestUsedLocation is used instead:
>>> btl.highestUsedLocation = 7
>>> btl.canAppend('123', addSpace=False)
True
>>> btl.canAppend('1234', addSpace=False)
False
'''
searchLocation = max(self.highestUsedLocation, self.textLocation)
addSpaceAmount = 1 if addSpace else 0
if (searchLocation + len(text) + addSpaceAmount) > self.lineLength:
return False
else:
return True
[docs]
def canInsert(self, textLocation, text):
'''
Returns True if there is enough space starting at textLocation to append
the text. False otherwise:
>>> btl = braille.text.BrailleTextLine(10)
>>> btl.canInsert(4, '123456')
True
>>> btl.canInsert(5, '123456')
False
'''
if textLocation + len(text) > self.lineLength:
return False
else:
return True
[docs]
def lastHyphenToSpace(self):
'''
Occasionally a line ends with a hyphen because
the last appender thought it would be helpful, such as
to put more characters into a line. But in case it
is not, then this method will change that last character
to a space and set textLocation back one character,
so it is not printed.
>>> bt = braille.text.BrailleTextLine(10)
>>> bt.append('hi', addSpace=False)
>>> bt.append(braille.lookup.symbols['music_hyphen'], addSpace=False)
>>> print(str(bt))
hi⠐
>>> bt.textLocation
3
>>> print(bt.allChars[2])
⠐
>>> bt.lastHyphenToSpace()
>>> print(str(bt))
hi
>>> bt.allChars[2] == braille.lookup.symbols['space']
True
>>> bt.textLocation
2
'''
prevLoc = self.textLocation - 1
if prevLoc < 0:
return
prevChar = self.allChars[prevLoc]
if prevChar == symbols['music_hyphen']: # and not forceHyphen:
self.allChars[prevLoc] = symbols['space']
self.textLocation -= 1
def __str__(self):
return ''.join(self.allChars[0:self.textLocation])
# ------------------------------------------------------------------------------
class BrailleTextException(exceptions21.Music21Exception):
pass
# ------------------------------------------------------------------------------
class Test(unittest.TestCase):
pass
if __name__ == '__main__':
import music21
music21.mainTest(Test)