1# -*- coding: utf-8 -*- 2# ------------------------------------------------------------------------------ 3# Name: bar.py 4# Purpose: music21 classes for representing bars, repeats, and related 5# 6# Authors: Michael Scott Cuthbert 7# Christopher Ariza 8# 9# Copyright: Copyright © 2009-2012, 2020 Michael Scott Cuthbert and the music21 Project 10# License: BSD, see license.txt 11# ------------------------------------------------------------------------------ 12''' 13Object models of barlines, including repeat barlines. 14''' 15import unittest 16from typing import Optional 17 18from music21 import base 19from music21 import exceptions21 20 21from music21 import expressions 22from music21 import repeat 23 24from music21 import environment 25 26_MOD = 'bar' 27environLocal = environment.Environment(_MOD) 28 29# ------------------------------------------------------------------------------ 30 31class BarException(exceptions21.Music21Exception): 32 pass 33 34 35# store alternative names for types; use this dictionary for translation 36# reference 37barTypeList = [ 38 'regular', 'dotted', 'dashed', 'heavy', 'double', 'final', 39 'heavy-light', 'heavy-heavy', 'tick', 'short', 'none', 40] 41 42# former are MusicXML names we allow 43barTypeDict = { 44 'light-light': 'double', 45 'light-heavy': 'final' 46} 47reverseBarTypeDict = { 48 'double': 'light-light', 49 'final': 'light-heavy', 50} 51 52 53def typeToMusicXMLBarStyle(value): 54 ''' 55 Convert a music21 barline name into the musicxml name -- 56 essentially just changes the names of 'double' and 'final' 57 to 'light-light' and 'light-heavy' 58 59 Does not do error checking to make sure it's a valid name, 60 since setting the style on a Barline object already does that. 61 62 >>> bar.typeToMusicXMLBarStyle('final') 63 'light-heavy' 64 >>> bar.typeToMusicXMLBarStyle('regular') 65 'regular' 66 ''' 67 if value.lower() in reverseBarTypeDict: 68 return reverseBarTypeDict[value.lower()] 69 else: 70 return value 71 72def standardizeBarType(value): 73 ''' 74 Standardizes bar type names. 75 76 converts all names to lower case, None to 'regular', 77 and 'light-light' to 'double' and 'light-heavy' to 'final', 78 raises an error for unknown styles. 79 ''' 80 if value is None: 81 return 'regular' # for now, return with string 82 83 value = value.lower() 84 85 if value in barTypeList: 86 return value 87 elif value in barTypeDict: 88 return barTypeDict[value] 89 # if not match 90 else: 91 raise BarException(f'cannot process style: {value}') 92 93 94# ------------------------------------------------------------------------------ 95class Barline(base.Music21Object): 96 '''A representation of a barline. 97 Barlines are conventionally assigned to Measure objects 98 using the leftBarline and rightBarline attributes. 99 100 101 >>> bl = bar.Barline('double') 102 >>> bl 103 <music21.bar.Barline type=double> 104 105 The type can also just be set via a keyword of "type". Or if no type is specified, 106 a regular barline is returned. Location can also be explicitly stored, but it's not 107 needed except for musicxml translation: 108 109 >>> bl2 = bar.Barline(type='dashed') 110 >>> bl2 111 <music21.bar.Barline type=dashed> 112 >>> bl3 = bar.Barline() 113 >>> bl3 114 <music21.bar.Barline type=regular> 115 >>> bl4 = bar.Barline(type='final', location='right') 116 >>> bl4 117 <music21.bar.Barline type=final> 118 >>> bl4.type 119 'final' 120 121 Note that the barline type 'ticked' only is displayed correctly in Finale and Finale Notepad. 122 123 N.B. for backwards compatibility reasons, currently 124 Bar objects do not use the style.Style class since 125 the phrase "style" was already used. 126 ''' 127 validStyles = list(barTypeDict.keys()) 128 129 classSortOrder = -5 130 131 def __init__(self, 132 type=None, # @ReservedAssignment # pylint: disable=redefined-builtin 133 location=None): 134 super().__init__() 135 136 self._type = None # same as style... 137 # this will raise an exception on error from property 138 self.type = type 139 140 # pause can be music21.expressions.Fermata object 141 self.pause = None 142 143 # location is primarily stored in the stream as leftBarline or rightBarline 144 # but can also be stored here. 145 self.location = location # musicxml values: can be left, right, middle, None 146 147 def _reprInternal(self): 148 return f'type={self.type}' 149 150 151 def _getType(self): 152 return self._type 153 154 def _setType(self, value): 155 self._type = standardizeBarType(value) 156 157 type = property(_getType, _setType, 158 doc=''' 159 Get and set the Barline type property. 160 161 >>> b = bar.Barline() 162 >>> b.type = 'tick' 163 >>> b.type 164 'tick' 165 166 Synonyms are given for some types, based on 167 musicxml styles: 168 169 >>> b.type = 'light-light' 170 >>> b.type 171 'double' 172 ''') 173 174 def musicXMLBarStyle(self): 175 ''' 176 returns the musicxml style for the bar. most are the same as 177 `.type` but "double" and "final" are different. 178 179 >>> b = bar.Barline('tick') 180 >>> b.musicXMLBarStyle() 181 'tick' 182 183 >>> b.type = 'double' 184 >>> b.musicXMLBarStyle() 185 'light-light' 186 187 >>> b.type = 'final' 188 >>> b.musicXMLBarStyle() 189 'light-heavy' 190 191 Changed in v.5.7 -- was a property before. 192 ''' 193 return typeToMusicXMLBarStyle(self.type) 194 195 196 197 198 199 200# ------------------------------------------------------------------------------ 201 202# note that musicxml permits the barline to have attributes for segno and coda 203# <xs:attribute name="segno" type="xs:token"/> 204# <xs:attribute name="coda" type="xs:token"/> 205 206# type <ending> in musicxml is used to mark different endings 207 208 209class Repeat(repeat.RepeatMark, Barline): 210 ''' 211 A Repeat barline. 212 213 The `direction` parameter can be one of `start` or `end`. 214 An `end` followed by a `start` 215 should be encoded as two `bar.Repeat` signs. 216 217 218 >>> rep = bar.Repeat(direction='end', times=3) 219 >>> rep 220 <music21.bar.Repeat direction=end times=3> 221 222 To apply a repeat barline assign it to either the `.leftBarline` or 223 `.rightBarline` attribute 224 of a measure. 225 226 >>> m = stream.Measure() 227 >>> m.leftBarline = bar.Repeat(direction='start') 228 >>> m.rightBarline = bar.Repeat(direction='end') 229 >>> m.insert(0.0, meter.TimeSignature('4/4')) 230 >>> m.repeatAppend(note.Note('D--5'), 4) 231 >>> p = stream.Part() 232 >>> p.insert(0.0, m) 233 >>> p.show('text') 234 {0.0} <music21.stream.Measure 0 offset=0.0> 235 {0.0} <music21.bar.Repeat direction=start> 236 {0.0} <music21.meter.TimeSignature 4/4> 237 {0.0} <music21.note.Note D--> 238 {1.0} <music21.note.Note D--> 239 {2.0} <music21.note.Note D--> 240 {3.0} <music21.note.Note D--> 241 {4.0} <music21.bar.Repeat direction=end> 242 243 The method :meth:`~music21.stream.Part.expandRepeats` on a 244 :class:`~music21.stream.Part` object expands the repeats, but 245 does not update measure numbers 246 247 >>> q = p.expandRepeats() 248 >>> q.show('text') 249 {0.0} <music21.stream.Measure 0 offset=0.0> 250 {0.0} <music21.bar.Barline type=double> 251 {0.0} <music21.meter.TimeSignature 4/4> 252 {0.0} <music21.note.Note D--> 253 {1.0} <music21.note.Note D--> 254 {2.0} <music21.note.Note D--> 255 {3.0} <music21.note.Note D--> 256 {4.0} <music21.bar.Barline type=double> 257 {4.0} <music21.stream.Measure 0a offset=4.0> 258 {0.0} <music21.bar.Barline type=double> 259 {0.0} <music21.meter.TimeSignature 4/4> 260 {0.0} <music21.note.Note D--> 261 {1.0} <music21.note.Note D--> 262 {2.0} <music21.note.Note D--> 263 {3.0} <music21.note.Note D--> 264 {4.0} <music21.bar.Barline type=double> 265 ''' 266 # _repeatDots = None # not sure what this is for; inherited from old modules 267 def __init__(self, direction='start', times=None): 268 repeat.RepeatMark.__init__(self) 269 if direction == 'start': 270 barType = 'heavy-light' 271 else: 272 barType = 'final' 273 Barline.__init__(self, type=barType) 274 275 self._direction: Optional[str] = None # either start or end 276 self._times: Optional[int] = None # if an end, how many repeats 277 278 # start is forward, end is backward in musicxml 279 self.direction = direction # start, end 280 self.times = times 281 282 def _reprInternal(self): 283 msg = f'direction={self.direction}' 284 if self.times is not None: 285 msg += f' times={self.times}' 286 return msg 287 288 289 @property 290 def direction(self) -> str: 291 ''' 292 Get or set the direction of this Repeat barline. Can be start or end. 293 294 TODO: show how changing direction changes type. 295 ''' 296 return self._direction 297 298 @direction.setter 299 def direction(self, value: str): 300 if value.lower() in ('start', 'end'): 301 self._direction = value.lower() 302 if self._direction == 'end': 303 self.type = 'final' 304 elif self._direction == 'start': 305 self.type = 'heavy-light' 306 else: 307 raise BarException(f'cannot set repeat direction to: {value}') 308 309 @property 310 def times(self) -> Optional[int]: 311 ''' 312 Get or set the times property of this barline. This 313 defines how many times the repeat happens. A standard repeat 314 repeats 2 times; values equal to or greater than 0 are permitted. 315 A repeat of 0 skips the repeated passage. 316 317 >>> lb = bar.Repeat(direction='start') 318 >>> rb = bar.Repeat(direction='end') 319 320 Only end expressions can have times: 321 322 >>> lb.times = 3 323 Traceback (most recent call last): 324 music21.bar.BarException: cannot set repeat times on a start Repeat 325 326 >>> rb.times = 3 327 >>> rb.times = -3 328 Traceback (most recent call last): 329 music21.bar.BarException: cannot set repeat times to a value less than zero: -3 330 ''' 331 return self._times 332 333 @times.setter 334 def times(self, value: int): 335 if value is None: 336 self._times = None 337 else: 338 try: 339 candidate = int(value) 340 except ValueError: 341 # pylint: disable:raise-missing-from 342 raise BarException( 343 f'cannot set repeat times to: {value!r}' 344 ) 345 346 if candidate < 0: 347 raise BarException( 348 f'cannot set repeat times to a value less than zero: {value}' 349 ) 350 if self.direction == 'start': 351 raise BarException('cannot set repeat times on a start Repeat') 352 353 self._times = candidate 354 355 356 def getTextExpression(self, prefix='', postfix='x'): 357 ''' 358 Return a configured :class:`~music21.expressions.TextExpressions` 359 object describing the repeat times. Append this to the stream 360 for annotation of repeat times. 361 362 >>> rb = bar.Repeat(direction='end') 363 >>> rb.times = 3 364 >>> rb.getTextExpression() 365 <music21.expressions.TextExpression '3x'> 366 367 >>> rb.getTextExpression(prefix='repeat ', postfix=' times') 368 <music21.expressions.TextExpression 'repeat 3 t...'> 369 ''' 370 value = f'{prefix}{self._times}{postfix}' 371 return expressions.TextExpression(value) 372 373 374# ------------------------------------------------------------------------------ 375class Test(unittest.TestCase): 376 377 def testSortOrder(self): 378 from music21 import stream 379 from music21 import clef 380 from music21 import note 381 from music21 import metadata 382 m = stream.Measure() 383 b = Repeat() 384 m.leftBarline = b 385 c = clef.BassClef() 386 m.append(c) 387 n = note.Note() 388 m.append(n) 389 390 # check sort order 391 self.assertEqual(m[0], b) 392 self.assertEqual(m[1], c) 393 self.assertEqual(m[2], n) 394 395 # if we add metadata, it sorts ahead of bar 396 md = metadata.Metadata() 397 m.insert(0, md) 398 399 self.assertEqual(m[0], md) 400 self.assertEqual(m[1], b) 401 402 def testFreezeThaw(self): 403 from music21 import converter 404 from music21 import stream 405 406 b = Barline() 407 self.assertNotIn('StyleMixin', b.classes) 408 s = stream.Stream([b]) 409 data = converter.freezeStr(s, fmt='pickle') 410 s2 = converter.thawStr(data) 411 thawedBarline = s2[0] 412 # Previously, raised AttributeError 413 self.assertEqual(thawedBarline.hasStyleInformation, False) 414 415 416if __name__ == '__main__': 417 import music21 418 music21.mainTest(Test) 419 420