1# -*- coding: utf-8 -*- 2# ------------------------------------------------------------------------------ 3# Name: graph/plots.py 4# Purpose: Classes for plotting music21 graphs based on Streams. 5# 6# Authors: Christopher Ariza 7# Michael Scott Cuthbert 8# Evan Lynch 9# 10# Copyright: Copyright © 2009-2012, 2017 Michael Scott Cuthbert and the music21 Project 11# License: BSD, see license.txt 12# ------------------------------------------------------------------------------ 13''' 14Object definitions for plotting :class:`~music21.stream.Stream` objects. 15 16The :class:`~music21.graph.plot.PlotStream` 17object subclasses combine a Graph object with the PlotStreamMixin to give 18reusable approaches to graphing data and structures in 19:class:`~music21.stream.Stream` objects. 20''' 21import collections 22import os 23import pathlib 24import unittest 25 26# from music21 import common 27from music21 import chord 28from music21 import common 29from music21 import corpus 30from music21 import converter 31from music21 import dynamics 32from music21 import features 33from music21 import note 34from music21 import prebase 35from music21 import stream # circular, but okay, because not used at top level. 36 37from music21.graph import axis 38from music21.graph import primitives 39from music21.graph.utilities import (GraphException, PlotStreamException) 40 41from music21.analysis import correlate 42from music21.analysis import discrete 43from music21.analysis import reduction 44from music21.analysis import windowed 45 46from music21 import environment 47_MOD = 'graph.plot' 48environLocal = environment.Environment(_MOD) 49 50 51def _mergeDicts(a, b): 52 '''utility function to merge two dictionaries''' 53 c = a.copy() 54 c.update(b) 55 return c 56 57 58# ------------------------------------------------------------------------------ 59# graphing utilities that operate on streams 60 61class PlotStreamMixin(prebase.ProtoM21Object): 62 ''' 63 This Mixin adds Stream extracting and Axis holding features to any 64 class derived from Graph. 65 ''' 66 axesClasses = {'x': axis.Axis, 'y': axis.Axis} 67 68 def __init__(self, streamObj=None, recurse=True, *args, **keywords): 69 # if not isinstance(streamObj, music21.stream.Stream): 70 if streamObj is not None and not hasattr(streamObj, 'elements'): # pragma: no cover 71 raise PlotStreamException(f'non-stream provided as argument: {streamObj}') 72 self.streamObj = streamObj 73 self.recurse = recurse 74 self.classFilterList = ['Note', 'Chord'] 75 self.matchPitchCountForChords = True 76 77 self.data = None # store native data representation, useful for testing 78 79 for axisName, axisClass in self.axesClasses.items(): 80 if axisClass is not None: 81 axisObj = axisClass(self, axisName) 82 setattr(self, 'axis' + axisName.upper(), axisObj) 83 84 self.savedKeywords = keywords 85 86 def _reprInternal(self) -> str: 87 # noinspection PyShadowingNames 88 ''' 89 The representation of the Plot shows the stream repr 90 in addition to the class name. 91 92 >>> st = stream.Stream() 93 >>> st.id = 'empty' 94 >>> plot = graph.plot.ScatterPitchClassQuarterLength(st) 95 >>> plot 96 <music21.graph.plot.ScatterPitchClassQuarterLength for <music21.stream.Stream empty>> 97 98 >>> plot = graph.plot.ScatterPitchClassQuarterLength(None) 99 >>> plot 100 <music21.graph.plot.ScatterPitchClassQuarterLength for (no stream)> 101 102 >>> plot.axisX 103 <music21.graph.axis.QuarterLengthAxis: x axis for ScatterPitchClassQuarterLength> 104 105 >>> plot.axisY 106 <music21.graph.axis.PitchClassAxis: y axis for ScatterPitchClassQuarterLength> 107 108 >>> axIsolated = graph.axis.DynamicsAxis(axisName='z') 109 >>> axIsolated 110 <music21.graph.axis.DynamicsAxis: z axis for (no client)> 111 ''' 112 s = self.streamObj 113 if s is not None: # not "if s" because could be empty 114 streamName = repr(s) 115 else: 116 streamName = '(no stream)' 117 118 return f'for {streamName}' 119 120 @property 121 def allAxes(self): 122 ''' 123 return a list of axisX, axisY, axisZ if any are defined in the class. 124 125 Some might be None. 126 127 >>> s = stream.Stream() 128 >>> p = graph.plot.ScatterPitchClassOffset(s) 129 >>> p.allAxes 130 [<music21.graph.axis.OffsetAxis: x axis for ScatterPitchClassOffset>, 131 <music21.graph.axis.PitchClassAxis: y axis for ScatterPitchClassOffset>] 132 ''' 133 allAxesList = [] 134 for axisName in ('axisX', 'axisY', 'axisZ'): 135 if hasattr(self, axisName): 136 allAxesList.append(getattr(self, axisName)) 137 return allAxesList 138 139 def run(self): 140 ''' 141 main routine to extract data, set axis labels, run process() on the underlying 142 Graph object, and if self.doneAction is not None, either write or show the graph. 143 ''' 144 self.setAxisKeywords() 145 self.extractData() 146 if hasattr(self, 'axisY') and self.axisY: 147 self.setTicks('y', self.axisY.ticks()) 148 self.setAxisLabel('y', self.axisY.label) 149 if hasattr(self, 'axisX') and self.axisX: 150 self.setTicks('x', self.axisX.ticks()) 151 self.setAxisLabel('x', self.axisX.label) 152 153 self.process() 154 155 # -------------------------------------------------------------------------- 156 def setAxisKeywords(self): 157 ''' 158 Configure axis parameters based on keywords given when creating the Plot. 159 160 Looks in self.savedKeywords, in case any post creation manipulation needs 161 to happen. 162 163 Finds keywords that begin with x, y, z and sets the remainder of the 164 keyword (lowercasing the first letter) as an attribute. Does not 165 set any new attributes, only existing ones. 166 167 >>> b = corpus.parse('bwv66.6') 168 >>> hist = graph.plot.HistogramPitchSpace(b, xHideUnused=False) 169 >>> hist.axisX.hideUnused 170 True 171 >>> hist.setAxisKeywords() 172 >>> hist.axisX.hideUnused 173 False 174 ''' 175 for thisAxis in self.allAxes: 176 if thisAxis is None: 177 continue 178 thisAxisLetter = thisAxis.axisName 179 for kw in self.savedKeywords: 180 if not kw.startswith(thisAxisLetter): 181 continue 182 if len(kw) < 3: 183 continue 184 shortKw = kw[1].lower() + kw[2:] 185 186 if not hasattr(thisAxis, shortKw): 187 continue 188 setattr(thisAxis, shortKw, self.savedKeywords[kw]) 189 190 # -------------------------------------------------------------------------- 191 192 def extractData(self): 193 if None in self.allAxes: 194 raise PlotStreamException('Set all axes before calling extractData() via run()') 195 196 if self.recurse: 197 sIter = self.streamObj.recurse() 198 else: 199 sIter = self.streamObj.iter() 200 201 if self.classFilterList: 202 sIter = sIter.getElementsByClass(self.classFilterList) 203 204 self.data = [] 205 206 for el in sIter: 207 dataList = self.processOneElement(el) 208 if dataList is not None: 209 self.data.extend(dataList) 210 211 self.postProcessData() 212 213 for i, thisAxis in enumerate(self.allAxes): 214 thisAxis.setBoundariesFromData([d[i] for d in self.data]) 215 216 def processOneElement(self, el): 217 ''' 218 Get a list of data from a single element (generally a Note or chord): 219 220 >>> n = note.Note('C#4') 221 >>> n.offset = 10.25 222 >>> s = stream.Stream([n]) 223 >>> pl = graph.plot.ScatterPitchClassOffset(s) 224 >>> pl.processOneElement(n) 225 [(10.25, 1, {})] 226 227 >>> c = chord.Chord(['D4', 'E5']) 228 >>> s.insert(5.0, c) 229 >>> pl.processOneElement(c) 230 [(5.0, 2, {}), (5.0, 4, {})] 231 232 ''' 233 elementValues = [[] for _ in range(len(self.allAxes))] 234 formatDict = {} 235 # should be two for most things... 236 237 if not isinstance(el, chord.Chord): 238 for i, thisAxis in enumerate(self.allAxes): 239 axisValue = thisAxis.extractOneElement(el, formatDict) 240 # use isinstance(List) not isiterable, since 241 # extractOneElement can distinguish between a tuple which 242 # represents a single value, or a list of values (or tuples) 243 # which represent multiple values 244 if not isinstance(axisValue, list) and axisValue is not None: 245 axisValue = [axisValue] 246 elementValues[i] = axisValue 247 else: 248 elementValues = self.extractChordDataMultiAxis(el, formatDict) 249 250 self.postProcessElement(el, formatDict, *elementValues) 251 if None in elementValues: 252 return None 253 254 elementValueLength = max([len(ev) for ev in elementValues]) 255 formatDictList = [formatDict.copy() for _ in range(elementValueLength)] 256 elementValues.append(formatDictList) 257 returnList = list(zip(*elementValues)) 258 return returnList 259 260 def postProcessElement(self, el, formatDict, *values): 261 pass 262 263 def postProcessData(self): 264 ''' 265 Call any post data processing routines here and on any axes. 266 ''' 267 for thisAxis in self.allAxes: 268 thisAxis.postProcessData() 269 270 # -------------------------------------------------------------------------- 271 @staticmethod 272 def extractChordDataOneAxis(ax, c, formatDict): 273 ''' 274 Look for Note-like attributes in a Chord. This is done by first 275 looking at the Chord, and then, if attributes are not found, looking at each pitch. 276 277 Returns a list of values. 278 279 280 ''' 281 values = [] 282 value = None 283 try: 284 value = ax.extractOneElement(c, formatDict) 285 except AttributeError: 286 pass # do not try others 287 288 if value is not None: 289 values.append(value) 290 291 if not values: # still not set, get form chord 292 for n in c: 293 # try to get get values from note inside chords 294 value = None 295 try: 296 value = ax.extractOneElement(n, formatDict) 297 except AttributeError: # pragma: no cover 298 break # do not try others 299 300 if value is not None: 301 values.append(value) 302 return values 303 304 def extractChordDataMultiAxis(self, c, formatDict): 305 ''' 306 Returns a list of lists of values for each axis. 307 ''' 308 elementValues = [self.extractChordDataOneAxis(ax, c, formatDict) for ax in self.allAxes] 309 310 lookIntoChordForNotesGroups = [] 311 for thisAxis, values in zip(self.allAxes, elementValues): 312 if not values: 313 lookIntoChordForNotesGroups.append((thisAxis, values)) 314 315 for thisAxis, destValues in lookIntoChordForNotesGroups: 316 for n in c: 317 try: 318 target = thisAxis.extractOneElement(n, formatDict) 319 except AttributeError: # pragma: no cover 320 continue # must try others 321 if target is not None: 322 destValues.append(target) 323 324 # environLocal.printDebug(['after looking at Pitch:', 325 # 'xValues', xValues, 'yValues', yValues]) 326 327 # if we only have one attribute from the Chord, and many from the 328 # Pitches, need to make the number of data points equal by 329 # duplicating data 330 if self.matchPitchCountForChords: 331 self.fillValueLists(elementValues) 332 return elementValues 333 334 @staticmethod 335 def fillValueLists(elementValues, nullFillValue=0): 336 ''' 337 pads a list of lists so that each list has the same length. 338 Pads with the first element of the list or nullFillValue if 339 the list has no elements. Modifies in place so returns None 340 341 Used by extractChordDataMultiAxis 342 343 >>> l0 = [2, 3, 4] 344 >>> l1 = [10, 20, 30, 40, 50] 345 >>> l2 = [] 346 >>> listOfLists = [l0, l1, l2] 347 >>> graph.plot.PlotStream.fillValueLists(listOfLists) 348 >>> listOfLists 349 [[2, 3, 4, 2, 2], 350 [10, 20, 30, 40, 50], 351 [0, 0, 0, 0, 0]] 352 ''' 353 maxLength = max([len(val) for val in elementValues]) 354 for val in elementValues: 355 shortAmount = maxLength - len(val) 356 if val: 357 fillVal = val[0] 358 else: 359 fillVal = nullFillValue 360 if shortAmount: 361 val += [fillVal] * shortAmount 362 363 # -------------------------------------------------------------------------- 364 @property 365 def id(self): 366 ''' 367 Each PlotStream has a unique id that consists of its class name and 368 the class names of the axes: 369 370 >>> s = stream.Stream() 371 >>> pScatter = graph.plot.ScatterPitchClassQuarterLength(s) 372 >>> pScatter.id 373 'scatter-quarterLength-pitchClass' 374 ''' 375 idName = self.graphType 376 377 for axisObj in self.allAxes: 378 if axisObj is None: 379 continue 380 axisName = axisObj.quantities[0] 381 idName += '-' + axisName 382 383 return idName 384 385 386# ------------------------------------------------------------------------------ 387 388class PlotStream(primitives.Graph, PlotStreamMixin): 389 def __init__(self, streamObj=None, *args, **keywords): 390 primitives.Graph.__init__(self, *args, **keywords) 391 PlotStreamMixin.__init__(self, streamObj, **keywords) 392 393 self.axisX = axis.OffsetAxis(self, 'x') 394 395 396# ------------------------------------------------------------------------------ 397# scatter plots 398 399class Scatter(primitives.GraphScatter, PlotStreamMixin): 400 ''' 401 Base class for 2D scatter plots. 402 ''' 403 404 def __init__(self, streamObj=None, *args, **keywords): 405 primitives.GraphScatter.__init__(self, *args, **keywords) 406 PlotStreamMixin.__init__(self, streamObj, **keywords) 407 408 409class ScatterPitchSpaceQuarterLength(Scatter): 410 r'''A scatter plot of pitch space and quarter length 411 412 413 >>> s = corpus.parse('bach/bwv324.xml') 414 >>> p = graph.plot.ScatterPitchSpaceQuarterLength(s) 415 >>> p.doneAction = None #_DOCS_HIDE 416 >>> p.id 417 'scatter-quarterLength-pitchSpace' 418 >>> p.run() 419 420 .. image:: images/ScatterPitchSpaceQuarterLength.* 421 :width: 600 422 ''' 423 axesClasses = {'x': axis.QuarterLengthAxis, 424 'y': axis.PitchSpaceAxis} 425 426 def __init__(self, streamObj=None, *args, **keywords): 427 super().__init__(streamObj, *args, **keywords) 428 self.axisX.useLogScale = True 429 # need more space for pitch axis labels 430 if 'figureSize' not in keywords: 431 self.figureSize = (6, 6) 432 if 'title' not in keywords: 433 self.title = 'Pitch by Quarter Length Scatter' 434# if 'alpha' not in keywords: 435# self.alpha = 0.7 436 437 438class ScatterPitchClassQuarterLength(ScatterPitchSpaceQuarterLength): 439 '''A scatter plot of pitch class and quarter length 440 441 >>> s = corpus.parse('bach/bwv324.xml') #_DOCS_HIDE 442 >>> p = graph.plot.ScatterPitchClassQuarterLength(s, doneAction=None) #_DOCS_HIDE 443 >>> #_DOCS_SHOW s = corpus.parse('bach/bwv57.8') 444 >>> #_DOCS_SHOW p = graph.plot.ScatterPitchClassQuarterLength(s) 445 >>> p.id 446 'scatter-quarterLength-pitchClass' 447 >>> p.run() 448 449 .. image:: images/ScatterPitchClassQuarterLength.* 450 :width: 600 451 ''' 452 axesClasses = {'x': axis.QuarterLengthAxis, 453 'y': axis.PitchClassAxis} 454 455 def __init__(self, streamObj=None, *args, **keywords): 456 super().__init__(streamObj, *args, **keywords) 457 if 'title' not in keywords: 458 self.title = 'Pitch Class by Quarter Length Scatter' 459 460 461class ScatterPitchClassOffset(Scatter): 462 '''A scatter plot of pitch class and offset 463 464 >>> s = corpus.parse('bach/bwv324.xml') #_DOCS_HIDE 465 >>> p = graph.plot.ScatterPitchClassOffset(s, doneAction=None) #_DOCS_HIDE 466 >>> #_DOCS_SHOW s = corpus.parse('bach/bwv57.8') 467 >>> #_DOCS_SHOW p = graph.plot.ScatterPitchClassOffset(s) 468 >>> p.id 469 'scatter-offset-pitchClass' 470 >>> p.run() 471 472 .. image:: images/ScatterPitchClassOffset.* 473 :width: 600 474 ''' 475 axesClasses = {'x': axis.OffsetAxis, 476 'y': axis.PitchClassAxis} 477 478 def __init__(self, streamObj=None, *args, **keywords): 479 super().__init__(streamObj, *args, **keywords) 480 481 # need more space for pitch axis labels 482 if 'figureSize' not in keywords: 483 self.figureSize = (10, 5) 484 if 'title' not in keywords: 485 self.title = 'Pitch Class by Offset Scatter' 486 if 'alpha' not in keywords: # will not restrike, so make less transparent 487 self.alpha = 0.7 488 489 490class ScatterPitchSpaceDynamicSymbol(Scatter): 491 ''' 492 A graph of dynamics used by pitch space. 493 494 >>> s = converter.parse('tinynotation: 4/4 C4 d E f', makeNotation=False) #_DOCS_HIDE 495 >>> s.insert(0.0, dynamics.Dynamic('pp')) #_DOCS_HIDE 496 >>> s.insert(2.0, dynamics.Dynamic('ff')) #_DOCS_HIDE 497 >>> p = graph.plot.ScatterPitchSpaceDynamicSymbol(s, doneAction=None) #_DOCS_HIDE 498 >>> #_DOCS_SHOW s = converter.parse('/Desktop/schumann/opus41no1/movement2.xml') 499 >>> #_DOCS_SHOW p = graph.plot.ScatterPitchSpaceDynamicSymbol(s) 500 >>> p.run() 501 502 .. image:: images/ScatterPitchSpaceDynamicSymbol.* 503 :width: 600 504 ''' 505 # string name used to access this class 506 figureSizeDefault = (12, 6) 507 axesClasses = {'x': axis.PitchSpaceAxis, 508 'y': axis.DynamicsAxis} 509 510 def __init__(self, streamObj=None, *args, **keywords): 511 super().__init__(streamObj, *args, **keywords) 512 513 self.axisX.showEnharmonic = False 514 # need more space for pitch axis labels 515 if 'figureSize' not in keywords: 516 self.figureSize = self.figureSizeDefault 517 if 'title' not in keywords: 518 self.title = 'Dynamics by Pitch Scatter' 519 if 'alpha' not in keywords: 520 self.alpha = 0.7 521 522 def extractData(self): 523 # get data from correlate object 524 am = correlate.ActivityMatch(self.streamObj) 525 amData = am.pitchToDynamic(dataPoints=True) 526 self.data = [] 527 for x, y in amData: 528 self.data.append((x, y, {})) 529 530 xVals = [d[0] for d in self.data] 531 yVals = [d[1] for d in self.data] 532 533 self.axisX.setBoundariesFromData(xVals) 534 self.axisY.setBoundariesFromData(yVals) 535 self.postProcessData() 536 537 538# ------------------------------------------------------------------------------ 539# histograms 540class Histogram(primitives.GraphHistogram, PlotStreamMixin): 541 ''' 542 Base class for histograms that plot one axis against its count 543 ''' 544 axesClasses = {'x': axis.Axis, 'y': axis.CountingAxis} 545 546 def __init__(self, streamObj=None, *args, **keywords): 547 primitives.GraphHistogram.__init__(self, *args, **keywords) 548 PlotStreamMixin.__init__(self, streamObj, **keywords) 549 550 if 'alpha' not in keywords: 551 self.alpha = 1.0 552 553 def run(self): 554 ''' 555 Override run method to remap X data into individual bins. 556 ''' 557 self.setAxisKeywords() 558 self.extractData() 559 self.setTicks('y', self.axisY.ticks()) 560 xTicksNew = self.remapXTicksData() 561 self.setTicks('x', xTicksNew) 562 self.setAxisLabel('y', self.axisY.label) 563 self.setAxisLabel('x', self.axisX.label) 564 565 self.process() 566 567 def remapXTicksData(self): 568 ''' 569 Changes the ticks and data so that they both run 570 1, 2, 3, 4, etc. 571 ''' 572 573 xTicksOrig = self.axisX.ticks() 574 xTickDict = {v[0]: v[1] for v in xTicksOrig} 575 xTicksNew = [] 576 # self.data is already sorted. 577 if ((not hasattr(self.axisX, 'hideUnused') or self.axisX.hideUnused is True) 578 or self.axisX.minValue is None 579 or self.axisX.maxValue is None): 580 for i in range(len(self.data)): 581 dataVal = self.data[i] 582 xDataVal = dataVal[0] 583 self.data[i] = (i + 1,) + dataVal[1:] 584 if xDataVal in xTickDict: # should be there: 585 newTick = (i + 1, xTickDict[xDataVal]) 586 xTicksNew.append(newTick) 587 else: 588 from music21 import pitch 589 for i in range(int(self.axisX.minValue), int(self.axisX.maxValue) + 1): 590 if i in xTickDict: 591 label = xTickDict[i] 592 elif hasattr(self.axisX, 'blankLabelUnused') and not self.axisX.blankLabelUnused: 593 label = pitch.Pitch(i).name 594 else: 595 label = '' 596 newTick = (i, label) 597 xTicksNew.append(newTick) 598 599 return xTicksNew 600 601 602class HistogramPitchSpace(Histogram): 603 '''A histogram of pitch space. 604 605 606 >>> s = corpus.parse('bach/bwv324.xml') #_DOCS_HIDE 607 >>> p = graph.plot.HistogramPitchSpace(s, doneAction=None) #_DOCS_HIDE 608 >>> #_DOCS_SHOW s = corpus.parse('bach/bwv57.8') 609 >>> #_DOCS_SHOW p = graph.plot.HistogramPitchSpace(s) 610 >>> p.id 611 'histogram-pitchSpace-count' 612 >>> p.run() # with defaults and proper configuration, will open graph 613 614 .. image:: images/HistogramPitchSpace.* 615 :width: 600 616 ''' 617 axesClasses = _mergeDicts(Histogram.axesClasses, {'x': axis.PitchSpaceAxis}) 618 619 def __init__(self, streamObj=None, *args, **keywords): 620 super().__init__(streamObj, *args, **keywords) 621 self.axisX.showEnharmonic = False 622 # need more space for pitch axis labels 623 if 'figureSize' not in keywords: 624 self.figureSize = (10, 6) 625 if 'title' not in keywords: 626 self.title = 'Pitch Histogram' 627 628 629class HistogramPitchClass(Histogram): 630 ''' 631 A histogram of pitch class 632 633 >>> s = corpus.parse('bach/bwv324.xml') #_DOCS_HIDE 634 >>> p = graph.plot.HistogramPitchClass(s, doneAction=None) #_DOCS_HIDE 635 >>> #_DOCS_SHOW s = corpus.parse('bach/bwv57.8') 636 >>> #_DOCS_SHOW p = graph.plot.HistogramPitchClass(s) 637 >>> p.id 638 'histogram-pitchClass-count' 639 >>> p.run() # with defaults and proper configuration, will open graph 640 641 .. image:: images/HistogramPitchClass.* 642 :width: 600 643 644 ''' 645 axesClasses = _mergeDicts(Histogram.axesClasses, {'x': axis.PitchClassAxis}) 646 647 def __init__(self, streamObj=None, *args, **keywords): 648 super().__init__(streamObj, *args, **keywords) 649 self.axisX.showEnharmonic = False 650 if 'title' not in keywords: 651 self.title = 'Pitch Class Histogram' 652 653 654class HistogramQuarterLength(Histogram): 655 '''A histogram of pitch class 656 657 658 >>> s = corpus.parse('bach/bwv324.xml') #_DOCS_HIDE 659 >>> p = graph.plot.HistogramQuarterLength(s, doneAction=None) #_DOCS_HIDE 660 >>> #_DOCS_SHOW s = corpus.parse('bach/bwv57.8') 661 >>> #_DOCS_SHOW p = graph.plot.HistogramQuarterLength(s) 662 >>> p.id 663 'histogram-quarterLength-count' 664 >>> p.run() # with defaults and proper configuration, will open graph 665 666 .. image:: images/HistogramQuarterLength.* 667 :width: 600 668 669 ''' 670 axesClasses = _mergeDicts(Histogram.axesClasses, {'x': axis.QuarterLengthAxis}) 671 672 def __init__(self, streamObj=None, *args, **keywords): 673 super().__init__(streamObj, *args, **keywords) 674 self.axisX = axis.QuarterLengthAxis(self, 'x') 675 self.axisX.useLogScale = False 676 if 'title' not in keywords: 677 self.title = 'Quarter Length Histogram' 678 679 680# ------------------------------------------------------------------------------ 681# weighted scatter 682 683class ScatterWeighted(primitives.GraphScatterWeighted, PlotStreamMixin): 684 ''' 685 Base class for histograms that plot one axis against its count. 686 687 The count is stored as the Z axis, though it is represented as size. 688 ''' 689 axesClasses = {'x': axis.Axis, 'y': axis.Axis, 'z': axis.CountingAxis} 690 691 def __init__(self, streamObj=None, *args, **keywords): 692 primitives.GraphScatterWeighted.__init__(self, *args, **keywords) 693 PlotStreamMixin.__init__(self, streamObj, **keywords) 694 695 self.axisZ.countAxes = ('x', 'y') 696 697 698class ScatterWeightedPitchSpaceQuarterLength(ScatterWeighted): 699 '''A graph of event, sorted by pitch, over time 700 701 702 >>> s = corpus.parse('bach/bwv324.xml') #_DOCS_HIDE 703 >>> p = graph.plot.ScatterWeightedPitchSpaceQuarterLength(s, doneAction=None) #_DOCS_HIDE 704 >>> #_DOCS_SHOW s = corpus.parse('bach/bwv57.8') 705 >>> #_DOCS_SHOW p = graph.plot.ScatterWeightedPitchSpaceQuarterLength(s) 706 >>> p.run() # with defaults and proper configuration, will open graph 707 708 .. image:: images/ScatterWeightedPitchSpaceQuarterLength.* 709 :width: 600 710 ''' 711 axesClasses = _mergeDicts(ScatterWeighted.axesClasses, {'x': axis.QuarterLengthAxis, 712 'y': axis.PitchSpaceAxis}) 713 714 def __init__(self, streamObj=None, *args, **keywords): 715 super().__init__( 716 streamObj, *args, **keywords) 717 # need more space for pitch axis labels 718 if 'figureSize' not in keywords: 719 self.figureSize = (7, 7) 720 if 'title' not in keywords: 721 self.title = 'Count of Pitch and Quarter Length' 722 if 'alpha' not in keywords: 723 self.alpha = 0.8 724 725 726class ScatterWeightedPitchClassQuarterLength(ScatterWeighted): 727 '''A graph of event, sorted by pitch class, over time. 728 729 730 >>> s = corpus.parse('bach/bwv324.xml') #_DOCS_HIDE 731 >>> p = graph.plot.ScatterWeightedPitchClassQuarterLength(s, doneAction=None) #_DOCS_HIDE 732 >>> #_DOCS_SHOW s = corpus.parse('bach/bwv57.8') 733 >>> #_DOCS_SHOW p = graph.plot.ScatterWeightedPitchClassQuarterLength(s) 734 >>> p.run() # with defaults and proper configuration, will open graph 735 736 .. image:: images/ScatterWeightedPitchClassQuarterLength.* 737 :width: 600 738 739 ''' 740 axesClasses = _mergeDicts(ScatterWeighted.axesClasses, {'x': axis.QuarterLengthAxis, 741 'y': axis.PitchClassAxis}) 742 743 def __init__(self, streamObj=None, *args, **keywords): 744 super().__init__( 745 streamObj, *args, **keywords) 746 747 # need more space for pitch axis labels 748 if 'figureSize' not in keywords: 749 self.figureSize = (7, 7) 750 if 'title' not in keywords: 751 self.title = 'Count of Pitch Class and Quarter Length' 752 if 'alpha' not in keywords: 753 self.alpha = 0.8 754 755 756class ScatterWeightedPitchSpaceDynamicSymbol(ScatterWeighted): 757 '''A graph of dynamics used by pitch space. 758 759 >>> #_DOCS_SHOW s = converter.parse('/Desktop/schumann/opus41no1/movement2.xml') 760 >>> s = converter.parse('tinynotation: 4/4 C4 d E f', makeNotation=False) #_DOCS_HIDE 761 >>> s.insert(0.0, dynamics.Dynamic('pp')) #_DOCS_HIDE 762 >>> s.insert(2.0, dynamics.Dynamic('ff')) #_DOCS_HIDE 763 >>> p = graph.plot.ScatterWeightedPitchSpaceDynamicSymbol(s, doneAction=None) #_DOCS_HIDE 764 >>> #_DOCS_SHOW p = graph.plot.ScatterWeightedPitchSpaceDynamicSymbol(s) 765 >>> p.run() # with defaults and proper configuration, will open graph 766 767 .. image:: images/ScatterWeightedPitchSpaceDynamicSymbol.* 768 :width: 600 769 770 ''' 771 axesClasses = _mergeDicts(ScatterWeighted.axesClasses, {'x': axis.PitchSpaceAxis, 772 'y': axis.DynamicsAxis, 773 }) 774 775 def __init__(self, streamObj=None, *args, **keywords): 776 super().__init__( 777 streamObj, *args, **keywords) 778 779 self.axisX.showEnharmonic = False 780 781 # need more space for pitch axis labels 782 if 'figureSize' not in keywords: 783 self.figureSize = (10, 10) 784 if 'title' not in keywords: 785 self.title = 'Count of Pitch Class and Quarter Length' 786 if 'alpha' not in keywords: 787 self.alpha = 0.8 788 # make smaller for axis display 789 if 'tickFontSize' not in keywords: 790 self.tickFontSize = 7 791 792 def extractData(self): 793 # get data from correlate object 794 am = correlate.ActivityMatch(self.streamObj) 795 self.data = am.pitchToDynamic(dataPoints=True) 796 xVals = [x for x, unused_y in self.data] 797 yVals = [y for unused_x, y in self.data] 798 self.data = [[x, y, 1] for x, y in self.data] 799 800 self.axisX.setBoundariesFromData(xVals) 801 self.axisY.setBoundariesFromData(yVals) 802 self.postProcessData() 803 804 805# ------------------------------------------------------------------------------ 806# color grids 807 808 809class WindowedAnalysis(primitives.GraphColorGrid, PlotStreamMixin): 810 ''' 811 Base Plot for windowed analysis routines such as Key Analysis or Ambitus. 812 ''' 813 format = 'colorGrid' 814 815 keywordConfigurables = primitives.GraphColorGrid.keywordConfigurables + ( 816 'minWindow', 'maxWindow', 'windowStep', 'windowType', 'compressLegend', 817 'processorClass', 'graphLegend') 818 819 axesClasses = {'x': axis.OffsetAxis, 'y': None} 820 processorClassDefault = discrete.KrumhanslSchmuckler 821 822 def __init__(self, streamObj=None, *args, **keywords): 823 self.processorClass = self.processorClassDefault # a discrete processor class. 824 self._processor = None 825 826 self.graphLegend = None 827 self.minWindow = 1 828 self.maxWindow = None 829 self.windowStep = 'pow2' 830 self.windowType = 'overlap' 831 self.compressLegend = True 832 833 primitives.GraphColorGrid.__init__(self, *args, **keywords) 834 PlotStreamMixin.__init__(self, streamObj, **keywords) 835 836 self.axisX = axis.OffsetAxis(self, 'x') 837 838 @property 839 def processor(self): 840 if not self.processorClass: 841 return None 842 if not self._processor: 843 self._processor = self.processorClass(self.streamObj) # pylint: disable=not-callable 844 return self._processor 845 846 def run(self, *args, **keywords): 847 ''' 848 actually create the graph... 849 ''' 850 if self.title == 'Music21 Graph' and self.processor: 851 self.title = (self.processor.name 852 + f' ({self.processor.solutionUnitString()})') 853 854 data, yTicks = self.extractData() 855 self.data = data 856 self.setTicks('y', yTicks) 857 858 self.axisX.setBoundariesFromData() 859 xTicks = self.axisX.ticks() 860 # replace offset values with 0 and 1, as proportional here 861 if len(xTicks) >= 2: 862 xTicks = [(0, xTicks[0][1]), (1, xTicks[-1][1])] 863 environLocal.printDebug(['xTicks', xTicks]) 864 self.setTicks('x', xTicks) 865 self.setAxisLabel('y', 'Window Size\n(Quarter Lengths)') 866 self.setAxisLabel('x', f'Windows ({self.axisX.label} Span)') 867 868 self.graphLegend = self._getLegend() 869 self.process() 870 871 # uses self.processor 872 873 def extractData(self): 874 ''' 875 Extract data actually calls the processing routine. 876 877 Returns two element tuple of the data (colorMatrix) and the yTicks list 878 ''' 879 wa = windowed.WindowedAnalysis(self.streamObj, self.processor) 880 unused_solutionMatrix, colorMatrix, metaMatrix = wa.process(self.minWindow, 881 self.maxWindow, 882 self.windowStep, 883 windowType=self.windowType) 884 885 # if more than 12 bars, reduce the number of ticks 886 if len(metaMatrix) > 12: 887 tickRange = range(0, len(metaMatrix), len(metaMatrix) // 12) 888 else: 889 tickRange = range(len(metaMatrix)) 890 891 environLocal.printDebug(['tickRange', tickRange]) 892 # environLocal.printDebug(['last start color', colorMatrix[-1][0]]) 893 894 # get dictionaries of meta data for each row 895 pos = 0 896 yTicks = [] 897 898 for y in tickRange: 899 thisWindowSize = metaMatrix[y]['windowSize'] 900 # pad three ticks for each needed 901 yTicks.append([pos, '']) # pad first 902 yTicks.append([pos + 1, str(thisWindowSize)]) 903 yTicks.append([pos + 2, '']) # pad last 904 pos += 3 905 906 return colorMatrix, yTicks 907 908 def _getLegend(self): 909 ''' 910 Returns a solution legend for a WindowedAnalysis 911 ''' 912 graphLegend = primitives.GraphColorGridLegend(doneAction=None, 913 title=self.title) 914 graphData = self.processor.solutionLegend(compress=self.compressLegend) 915 graphLegend.data = graphData 916 return graphLegend 917 918 def write(self, fp=None): # pragma: no cover 919 ''' 920 Overrides the normal write method here to add a legend. 921 ''' 922 # call the process routine in the base graph 923 super().write(fp) 924 925 if fp is None: 926 fp = environLocal.getTempFile('.png', returnPathlib=True) 927 else: 928 fp = common.cleanpath(fp, returnPathlib=True) 929 930 directory, fn = os.path.split(fp) 931 fpLegend = os.path.join(directory, 'legend-' + fn) 932 # create a new graph of the legend 933 self.graphLegend.process() 934 self.graphLegend.write(fpLegend) 935 936 937class WindowedKey(WindowedAnalysis): 938 ''' 939 Stream plotting of windowed version of Krumhansl-Schmuckler analysis routine. 940 See :class:`~music21.analysis.discrete.KrumhanslSchmuckler` for more details. 941 942 943 >>> s = corpus.parse('bach/bwv66.6') 944 >>> p = graph.plot.WindowedKey(s.parts[0]) 945 >>> p.doneAction = None #_DOCS_HIDE 946 >>> p.run() # with defaults and proper configuration, will open graph 947 948 .. image:: images/WindowedKrumhanslSchmuckler.* 949 :width: 600 950 951 .. image:: images/legend-WindowedKrumhanslSchmuckler.* 952 953 Set the processor class to one of the following for different uses: 954 955 >>> p = graph.plot.WindowedKey(s.parts.first()) 956 >>> p.processorClass = analysis.discrete.AardenEssen 957 >>> p.processorClass = analysis.discrete.SimpleWeights 958 >>> p.processorClass = analysis.discrete.BellmanBudge 959 >>> p.processorClass = analysis.discrete.TemperleyKostkaPayne 960 >>> p.doneAction = None #_DOCS_HIDE 961 >>> p.run() 962 963 ''' 964 processorClassDefault = discrete.KrumhanslSchmuckler 965 966 967class WindowedAmbitus(WindowedAnalysis): 968 ''' 969 Stream plotting of basic pitch span. 970 971 >>> s = corpus.parse('bach/bwv66.6') 972 >>> p = graph.plot.WindowedAmbitus(s.parts.first()) 973 >>> p.doneAction = None #_DOCS_HIDE 974 >>> p.run() # with defaults and proper configuration, will open graph 975 976 .. image:: images/WindowedAmbitus.* 977 :width: 600 978 979 .. image:: images/legend-WindowedAmbitus.* 980 981 ''' 982 processorClassDefault = discrete.Ambitus 983 984# ------------------------------------------------------------------------------ 985# horizontal bar graphs 986 987 988class HorizontalBar(primitives.GraphHorizontalBar, PlotStreamMixin): 989 ''' 990 A graph of events, sorted by pitch, over time 991 ''' 992 axesClasses = {'x': axis.OffsetEndAxis, 'y': axis.PitchSpaceAxis} 993 994 def __init__(self, streamObj=None, *args, **keywords): 995 primitives.GraphHorizontalBar.__init__(self, *args, **keywords) 996 PlotStreamMixin.__init__(self, streamObj, **keywords) 997 998 self.axisY.hideUnused = False 999 1000 def postProcessData(self): 1001 ''' 1002 Call any post data processing routines here and on any axes. 1003 ''' 1004 super().postProcessData() 1005 self.axisY.setBoundariesFromData([d[1] for d in self.data]) 1006 yTicks = self.axisY.ticks() 1007 1008 pitchSpanDict = {} 1009 newData = [] 1010 dictOfFormatDicts = {} 1011 1012 for positionData, pitchData, formatDict in self.data: 1013 if pitchData not in pitchSpanDict: 1014 pitchSpanDict[pitchData] = [] 1015 dictOfFormatDicts[pitchData] = {} 1016 1017 pitchSpanDict[pitchData].append(positionData) 1018 _mergeDicts(dictOfFormatDicts[pitchData], formatDict) 1019 1020 for unused_k, v in pitchSpanDict.items(): 1021 v.sort() # sort these tuples. 1022 1023 for numericValue, label in yTicks: 1024 if numericValue in pitchSpanDict: 1025 newData.append([label, 1026 pitchSpanDict[numericValue], 1027 dictOfFormatDicts[numericValue]]) 1028 else: 1029 newData.append([label, [], {}]) 1030 self.data = newData 1031 1032 1033class HorizontalBarPitchClassOffset(HorizontalBar): 1034 '''A graph of events, sorted by pitch class, over time 1035 1036 1037 >>> s = corpus.parse('bach/bwv324.xml') #_DOCS_HIDE 1038 >>> p = graph.plot.HorizontalBarPitchClassOffset(s, doneAction=None) #_DOCS_HIDE 1039 >>> #_DOCS_SHOW s = corpus.parse('bach/bwv57.8') 1040 >>> #_DOCS_SHOW p = graph.plot.HorizontalBarPitchClassOffset(s) 1041 >>> p.run() # with defaults and proper configuration, will open graph 1042 1043 .. image:: images/HorizontalBarPitchClassOffset.* 1044 :width: 600 1045 1046 ''' 1047 axesClasses = _mergeDicts(HorizontalBar.axesClasses, {'y': axis.PitchClassAxis}) 1048 1049 def __init__(self, streamObj=None, *args, **keywords): 1050 super().__init__(streamObj, *args, **keywords) 1051 self.axisY = axis.PitchClassAxis(self, 'y') 1052 self.axisY.hideUnused = False 1053 1054 # need more space for pitch axis labels 1055 if 'figureSize' not in keywords: 1056 self.figureSize = (10, 4) 1057 if 'title' not in keywords: 1058 self.title = 'Note Quarter Length and Offset by Pitch Class' 1059 1060 1061class HorizontalBarPitchSpaceOffset(HorizontalBar): 1062 '''A graph of events, sorted by pitch space, over time 1063 1064 1065 >>> s = corpus.parse('bach/bwv324.xml') #_DOCS_HIDE 1066 >>> p = graph.plot.HorizontalBarPitchSpaceOffset(s, doneAction=None) #_DOCS_HIDE 1067 >>> #_DOCS_SHOW s = corpus.parse('bach/bwv57.8') 1068 >>> #_DOCS_SHOW p = graph.plot.HorizontalBarPitchSpaceOffset(s) 1069 >>> p.run() # with defaults and proper configuration, will open graph 1070 1071 .. image:: images/HorizontalBarPitchSpaceOffset.* 1072 :width: 600 1073 ''' 1074 1075 def __init__(self, streamObj=None, *args, **keywords): 1076 super().__init__(streamObj, *args, **keywords) 1077 1078 if 'figureSize' not in keywords: 1079 self.figureSize = (10, 6) 1080 if 'title' not in keywords: 1081 self.title = 'Note Quarter Length by Pitch' 1082 1083 1084# ------------------------------------------------------------------------------ 1085class HorizontalBarWeighted(primitives.GraphHorizontalBarWeighted, PlotStreamMixin): 1086 ''' 1087 A base class for plots of Scores with weighted (by height) horizontal bars. 1088 Many different weighted segments can provide a 1089 representation of a dynamic parameter of a Part. 1090 ''' 1091 axesClasses = { 1092 'x': axis.OffsetAxis, 1093 'y': None 1094 } 1095 keywordConfigurables = primitives.GraphHorizontalBarWeighted.keywordConfigurables + ( 1096 'fillByMeasure', 'segmentByTarget', 'normalizeByPart', 'partGroups') 1097 1098 def __init__(self, streamObj=None, *args, **keywords): 1099 self.fillByMeasure = False 1100 self.segmentByTarget = True 1101 self.normalizeByPart = False 1102 self.partGroups = None 1103 1104 primitives.GraphHorizontalBarWeighted.__init__(self, *args, **keywords) 1105 PlotStreamMixin.__init__(self, streamObj, **keywords) 1106 1107 def extractData(self): 1108 ''' 1109 Extract the data from the Stream. 1110 ''' 1111 if not isinstance(self.streamObj, stream.Score): 1112 raise GraphException('provided Stream must be Score') 1113 # parameters: x, span, heightScalar, color, alpha, yShift 1114 pr = reduction.PartReduction( 1115 self.streamObj, 1116 partGroups=self.partGroups, 1117 fillByMeasure=self.fillByMeasure, 1118 segmentByTarget=self.segmentByTarget, 1119 normalizeByPart=self.normalizeByPart) 1120 pr.process() 1121 data = pr.getGraphHorizontalBarWeightedData() 1122 # environLocal.printDebug(['data', data]) 1123 uniqueOffsets = [] 1124 for unused_key, value in data: 1125 for dataList in value: 1126 start = dataList[0] 1127 dur = dataList[1] 1128 if start not in uniqueOffsets: 1129 uniqueOffsets.append(start) 1130 if start + dur not in uniqueOffsets: 1131 uniqueOffsets.append(start + dur) 1132 # use default args for now 1133 self.axisX.minValue = min(uniqueOffsets) 1134 self.axisX.maxValue = max(uniqueOffsets) 1135 self.data = data 1136 1137 1138class Dolan(HorizontalBarWeighted): 1139 ''' 1140 A graph of the activity of a parameter of a part (or a group of parts) over time. 1141 The default parameter graphed is Dynamics. Dynamics are assumed to extend activity 1142 to the next change in dynamics. 1143 1144 Numerous parameters can be configured based on functionality encoded in 1145 the :class:`~music21.analysis.reduction.PartReduction` object. 1146 1147 1148 If the `fillByMeasure` parameter is True, and if measures are available, each part 1149 will segment by Measure divisions, and look for the target activity only once per 1150 Measure. If more than one target is found in the Measure, values will be averaged. 1151 If `fillByMeasure` is False, the part will be segmented by each Note. 1152 1153 The `segmentByTarget` parameter is True, segments, which may be Notes or Measures, 1154 will be divided if necessary to show changes that occur over the duration of the 1155 segment by a target object. 1156 1157 If the `normalizeByPart` parameter is True, each part will be normalized within the 1158 range only of that part. If False, all parts will be normalized by the max of all parts. 1159 The default is True. 1160 1161 >>> s = corpus.parse('bwv66.6') 1162 >>> dyn = ['p', 'mf', 'f', 'ff', 'mp', 'fff', 'ppp'] 1163 >>> i = 0 1164 >>> for p in s.parts: 1165 ... for m in p.getElementsByClass('Measure'): 1166 ... m.insert(0, dynamics.Dynamic(dyn[i % len(dyn)])) 1167 ... i += 1 1168 ... 1169 >>> #_DOCS_SHOW s.plot('dolan', fillByMeasure=True, segmentByTarget=True) 1170 1171 .. image:: images/Dolan.* 1172 :width: 600 1173 1174 ''' 1175 1176 def __init__(self, streamObj=None, *args, **keywords): 1177 super().__init__(streamObj, *args, **keywords) 1178 1179 # self.fy = lambda n: n.pitch.pitchClass 1180 # self.fyTicks = self.ticksPitchClassUsage 1181 # must set part groups if not defined here 1182 if streamObj is not None: 1183 self._getPartGroups() 1184 # need more space for pitch axis labels 1185 if 'figureSize' not in keywords: 1186 self.figureSize = (10, 4) 1187 1188 if 'title' not in keywords: 1189 self.title = 'Instrumentation' 1190 if self.streamObj and self.streamObj.metadata is not None: 1191 if self.streamObj.metadata.title is not None: 1192 self.title = self.streamObj.metadata.title 1193 if 'hideYGrid' not in keywords: 1194 self.hideYGrid = True 1195 1196 def _getPartGroups(self): 1197 ''' 1198 Examine the instruments in the Score and determine if there 1199 is a good match for a default configuration of parts. 1200 ''' 1201 if self.partGroups: 1202 return # keep what the user set 1203 if self.streamObj: 1204 return None 1205 instStream = self.streamObj.flatten().getElementsByClass('Instrument') 1206 if not instStream: 1207 return # do not set anything 1208 1209 if len(instStream) == 4 and self.streamObj.getElementById('Soprano') is not None: 1210 pgOrc = [ 1211 {'name': 'Soprano', 'color': 'purple', 'match': ['soprano', '0']}, 1212 {'name': 'Alto', 'color': 'orange', 'match': ['alto', '1']}, 1213 {'name': 'Tenor', 'color': 'lightgreen', 'match': ['tenor']}, 1214 {'name': 'Bass', 'color': 'mediumblue', 'match': ['bass']}, 1215 ] 1216 self.partGroups = pgOrc 1217 1218 elif len(instStream) == 4 and self.streamObj.getElementById('Viola') is not None: 1219 pgOrc = [ 1220 {'name': '1st Violin', 'color': 'purple', 1221 'match': ['1st violin', '0', 'violin 1', 'violin i']}, 1222 {'name': '2nd Violin', 'color': 'orange', 1223 'match': ['2nd violin', '1', 'violin 2', 'violin ii']}, 1224 {'name': 'Viola', 'color': 'lightgreen', 'match': ['viola']}, 1225 {'name': 'Cello', 'color': 'mediumblue', 1226 'match': ['cello', 'violoncello', "'cello"]}, 1227 ] 1228 self.partGroups = pgOrc 1229 1230 elif len(instStream) > 10: 1231 pgOrc = [ 1232 {'name': 'Flute', 'color': '#C154C1', 'match': ['flauto', r'flute \d']}, 1233 {'name': 'Oboe', 'color': 'blue', 'match': ['oboe', r'oboe \d']}, 1234 {'name': 'Clarinet', 'color': 'mediumblue', 1235 'match': ['clarinetto', r'clarinet in \w* \d']}, 1236 {'name': 'Bassoon', 'color': 'purple', 'match': ['fagotto', r'bassoon \d']}, 1237 1238 {'name': 'Horns', 'color': 'orange', 'match': ['corno', r'horn in \w* \d']}, 1239 {'name': 'Trumpet', 'color': 'red', 1240 'match': ['tromba', r'trumpet \d', r'trumpet in \w* \d']}, 1241 {'name': 'Trombone', 'color': 'red', 'match': [r'trombone \d']}, 1242 {'name': 'Timpani', 'color': '#5C3317', 'match': None}, 1243 1244 1245 {'name': 'Violin I', 'color': 'lightgreen', 'match': ['violino i', 'violin i']}, 1246 {'name': 'Violin II', 'color': 'green', 'match': ['violino ii', 'violin ii']}, 1247 {'name': 'Viola', 'color': 'forestgreen', 'match': None}, 1248 {'name': 'Violoncello & CB', 'color': 'dark green', 1249 'match': ['violoncello', 'contrabasso']}, 1250 # {'name':'CB', 'color':'#003000', 'match':['contrabasso']}, 1251 ] 1252 self.partGroups = pgOrc 1253 1254 1255# ------------------------------------------------------------------------------------------ 1256# 3D plots 1257 1258class Plot3DBars(primitives.Graph3DBars, PlotStreamMixin): 1259 ''' 1260 Base class for Stream plotting classes. 1261 ''' 1262 axesClasses = {'x': axis.QuarterLengthAxis, 1263 'y': axis.PitchClassAxis, 1264 'z': axis.CountingAxis, } 1265 1266 def __init__(self, streamObj=None, *args, **keywords): 1267 primitives.Graph3DBars.__init__(self, *args, **keywords) 1268 PlotStreamMixin.__init__(self, streamObj, **keywords) 1269 1270 self.axisZ.countAxes = ('x', 'y') 1271 1272 1273class Plot3DBarsPitchSpaceQuarterLength(Plot3DBars): 1274 ''' 1275 A scatter plot of pitch and quarter length 1276 1277 >>> s = corpus.parse('bach/bwv324.xml') #_DOCS_HIDE 1278 >>> p = graph.plot.Plot3DBarsPitchSpaceQuarterLength(s, doneAction=None) #_DOCS_HIDE 1279 >>> #_DOCS_SHOW from music21.musicxml import testFiles 1280 >>> #_DOCS_SHOW s = converter.parse(testFiles.mozartTrioK581Excerpt) 1281 >>> #_DOCS_SHOW p = graph.plot.Plot3DBarsPitchSpaceQuarterLength(s) 1282 >>> p.id 1283 '3DBars-quarterLength-pitchSpace-count' 1284 >>> p.run() # with defaults and proper configuration, will open graph 1285 1286 .. image:: images/Plot3DBarsPitchSpaceQuarterLength.* 1287 :width: 600 1288 ''' 1289 axesClasses = _mergeDicts(Plot3DBars.axesClasses, {'y': axis.PitchSpaceAxis}) 1290 1291 def __init__(self, streamObj=None, *args, **keywords): 1292 super().__init__(streamObj, *args, **keywords) 1293 1294 # need more space for pitch axis labels 1295 if 'figureSize' not in keywords: 1296 self.figureSize = (6, 6) 1297 if 'title' not in keywords: 1298 self.title = 'Pitch by Quarter Length Count' 1299 1300 1301# ------------------------------------------------------------------------------ 1302# base class for multi-stream displays 1303 1304class MultiStream(primitives.GraphGroupedVerticalBar, PlotStreamMixin): 1305 ''' 1306 Approaches to plotting and graphing multiple Streams. 1307 A base class from which Stream plotting Classes inherit. 1308 1309 Not yet integrated into the new 2017 system, unfortunately... 1310 1311 Provide a list of Streams as an argument. Optionally 1312 provide an additional list of labels for each list. 1313 ''' 1314 axesClasses = {} 1315 1316 def __init__(self, streamList, labelList=None, *args, **keywords): 1317 primitives.GraphGroupedVerticalBar.__init__(self, *args, **keywords) 1318 PlotStreamMixin.__init__(self, None) 1319 1320 if labelList is None: 1321 labelList = [] 1322 self.streamList = None 1323 foundPaths = self.parseStreams(streamList) 1324 1325 # use found paths if no labels are provided 1326 if not labelList and len(foundPaths) == len(streamList): 1327 self.labelList = foundPaths 1328 else: 1329 self.labelList = labelList 1330 1331 self.data = None # store native data representation, useful for testing 1332 1333 def parseStreams(self, streamList): 1334 self.streamList = [] 1335 foundPaths = [] 1336 for s in streamList: 1337 # could be corpus or file path 1338 if isinstance(s, str): 1339 foundPaths.append(os.path.basename(s)) 1340 if os.path.exists(s): 1341 s = converter.parse(s) 1342 else: # assume corpus 1343 s = corpus.parse(s) 1344 elif isinstance(s, pathlib.Path): 1345 foundPaths.append(s.name) 1346 if s.exists(): 1347 s = converter.parse(s) 1348 else: # assume corpus 1349 s = corpus.parse(s) 1350 # otherwise assume a parsed stream 1351 self.streamList.append(s) 1352 return foundPaths 1353 1354 1355class Features(MultiStream): 1356 ''' 1357 Plots the output of a set of feature extractors. 1358 1359 FeatureExtractors can be ids or classes. 1360 ''' 1361 format = 'features' 1362 1363 def __init__(self, streamList, featureExtractors, labelList=None, *args, **keywords): 1364 if labelList is None: 1365 labelList = [] 1366 1367 super().__init__(streamList, labelList, *args, **keywords) 1368 1369 self.featureExtractors = featureExtractors 1370 1371 self.xTickLabelRotation = 90 1372 self.xTickLabelHorizontalAlignment = 'left' 1373 self.xTickLabelVerticalAlignment = 'top' 1374 1375 # self.graph.setAxisLabel('y', 'Count') 1376 # self.graph.setAxisLabel('x', 'Streams') 1377 1378 # need more space for pitch axis labels 1379 if 'figureSize' not in keywords: 1380 self.figureSize = (10, 6) 1381 if 'title' not in keywords: 1382 self.title = None 1383 1384 def run(self): 1385 # will use self.fx and self.fxTick to extract data 1386 self.setAxisKeywords() 1387 1388 self.data, xTicks, yTicks = self.extractData() 1389 1390 self.grid = False 1391 1392 self.setTicks('x', xTicks) 1393 self.setTicks('y', yTicks) 1394 self.process() 1395 1396 def extractData(self): 1397 if len(self.labelList) != len(self.streamList): 1398 labelList = [x + 1 for x in range(len(self.streamList))] 1399 else: 1400 labelList = self.labelList 1401 1402 feList = [] 1403 for fe in self.featureExtractors: 1404 if isinstance(fe, str): 1405 post = features.extractorsById(fe) 1406 for sub in post: 1407 feList.append(sub()) 1408 else: # assume a class 1409 feList.append(fe()) 1410 1411 # store each stream in a data instance 1412 diList = [] 1413 for s in self.streamList: 1414 di = features.DataInstance(s) 1415 diList.append(di) 1416 1417 data = [] 1418 for i, di in enumerate(diList): 1419 sub = collections.OrderedDict() 1420 for fe in feList: 1421 fe.data = di 1422 v = fe.extract().vector 1423 if len(v) == 1: 1424 sub[fe.name] = v[0] 1425 # average all values? 1426 else: 1427 sub[fe.name] = sum(v) / len(v) 1428 dataPoint = [labelList[i], sub] 1429 data.append(dataPoint) 1430 1431 # environLocal.printDebug(['data', data]) 1432 1433 xTicks = [] 1434 for x, label in enumerate(labelList): 1435 # first value needs to be center of bar 1436 # value of tick is the string entry 1437 xTicks.append([x + 0.5, f'{label}']) 1438 # always have min and max 1439 yTicks = [] 1440 return data, xTicks, yTicks 1441 1442# ----------------------------------------------------------------------------------- 1443 1444 1445class TestExternalManual(unittest.TestCase): # pragma: no cover 1446 1447 def testHorizontalBarPitchSpaceOffset(self): 1448 a = corpus.parse('bach/bwv57.8') 1449 # do not need to call flat version 1450 b = HorizontalBarPitchSpaceOffset(a.parts[0], title='Bach (soprano voice)') 1451 b.run() 1452 1453 b = HorizontalBarPitchSpaceOffset(a, title='Bach (all parts)') 1454 b.run() 1455 1456 def testHorizontalBarPitchClassOffset(self): 1457 a = corpus.parse('bach/bwv57.8') 1458 b = HorizontalBarPitchClassOffset(a.parts[0], title='Bach (soprano voice)') 1459 b.run() 1460 1461 a = corpus.parse('bach/bwv57.8') 1462 b = HorizontalBarPitchClassOffset(a.parts[0].measures(3, 6), 1463 title='Bach (soprano voice, mm 3-6)') 1464 b.run() 1465 1466 def testScatterWeightedPitchSpaceQuarterLength(self): 1467 a = corpus.parse('bach/bwv57.8').parts[0].flatten() 1468 for xLog in [True, False]: 1469 b = ScatterWeightedPitchSpaceQuarterLength( 1470 a, title='Pitch Space Bach (soprano voice)', 1471 ) 1472 b.axisX.useLogScale = xLog 1473 b.run() 1474 1475 b = ScatterWeightedPitchClassQuarterLength( 1476 a, title='Pitch Class Bach (soprano voice)', 1477 ) 1478 b.axisX.useLogScale = xLog 1479 b.run() 1480 1481 def testPitchSpace(self): 1482 a = corpus.parse('bach/bwv57.8') 1483 b = HistogramPitchSpace(a.parts[0].flatten(), title='Bach (soprano voice)') 1484 b.run() 1485 1486 def testPitchClass(self): 1487 a = corpus.parse('bach/bwv57.8') 1488 b = HistogramPitchClass(a.parts[0].flatten(), title='Bach (soprano voice)') 1489 b.run() 1490 1491 def testQuarterLength(self): 1492 a = corpus.parse('bach/bwv57.8') 1493 b = HistogramQuarterLength(a.parts[0].flatten(), title='Bach (soprano voice)') 1494 b.run() 1495 1496 def testScatterPitchSpaceQuarterLength(self): 1497 for xLog in [True, False]: 1498 1499 a = corpus.parse('bach/bwv57.8') 1500 b = ScatterPitchSpaceQuarterLength(a.parts[0].flatten(), title='Bach (soprano voice)', 1501 ) 1502 b.axisX.useLogScale = xLog 1503 b.run() 1504 1505 b = ScatterPitchClassQuarterLength(a.parts[0].flatten(), title='Bach (soprano voice)', 1506 ) 1507 b.axisX.useLogScale = xLog 1508 b.run() 1509 1510 def testScatterPitchClassOffset(self): 1511 a = corpus.parse('bach/bwv57.8') 1512 b = ScatterPitchClassOffset(a.parts[0].flatten(), title='Bach (soprano voice)') 1513 b.run() 1514 1515 def testScatterPitchSpaceDynamicSymbol(self): 1516 a = corpus.parse('schumann/opus41no1', 2) 1517 b = ScatterPitchSpaceDynamicSymbol(a.parts[0].flatten(), title='Schumann (soprano voice)') 1518 b.run() 1519 1520 b = ScatterWeightedPitchSpaceDynamicSymbol(a.parts[0].flatten(), 1521 title='Schumann (soprano voice)') 1522 b.run() 1523 1524 def testPlot3DPitchSpaceQuarterLengthCount(self): 1525 a = corpus.parse('schoenberg/opus19', 6) # also tests Tuplets 1526 b = Plot3DBarsPitchSpaceQuarterLength(a.flatten().stripTies(), 1527 title='Schoenberg pitch space') 1528 b.run() 1529 1530 def writeAllPlots(self): 1531 ''' 1532 Write a graphic file for all graphs, naming them after the appropriate class. 1533 This is used to generate documentation samples. 1534 ''' 1535 # TODO: need to add strip() ties here; but need stripTies on Score 1536 from music21.musicxml import testFiles 1537 1538 plotClasses = [ 1539 # histograms 1540 (HistogramPitchSpace, None, None), 1541 (HistogramPitchClass, None, None), 1542 (HistogramQuarterLength, None, None), 1543 # scatters 1544 (ScatterPitchSpaceQuarterLength, None, None), 1545 (ScatterPitchClassQuarterLength, None, None), 1546 (ScatterPitchClassOffset, None, None), 1547 (ScatterPitchSpaceDynamicSymbol, 1548 corpus.getWork('schumann/opus41no1', 2), 1549 'Schumann Opus 41 No 1'), 1550 1551 # offset based horizontal 1552 (HorizontalBarPitchSpaceOffset, None, None), 1553 (HorizontalBarPitchClassOffset, None, None), 1554 # weighted scatter 1555 (ScatterWeightedPitchSpaceQuarterLength, None, None), 1556 (ScatterWeightedPitchClassQuarterLength, None, None), 1557 (ScatterWeightedPitchSpaceDynamicSymbol, 1558 corpus.getWork('schumann/opus41no1', 2), 1559 'Schumann Opus 41 No 1'), 1560 1561 1562 # 3d graphs 1563 (Plot3DBarsPitchSpaceQuarterLength, 1564 testFiles.mozartTrioK581Excerpt, 1565 'Mozart Trio K581 Excerpt'), 1566 1567 (WindowedKey, corpus.getWork('bach/bwv66.6.xml'), 'Bach BWV 66.6'), 1568 (WindowedAmbitus, corpus.getWork('bach/bwv66.6.xml'), 'Bach BWV 66.6'), 1569 1570 ] 1571 1572 sDefault = corpus.parse('bach/bwv57.8') 1573 1574 for plotClassName, work, titleStr in plotClasses: 1575 if work is None: 1576 s = sDefault 1577 1578 else: # expecting data 1579 s = converter.parse(work) 1580 1581 if titleStr is not None: 1582 obj = plotClassName(s, doneAction=None, title=titleStr) 1583 else: 1584 obj = plotClassName(s, doneAction=None) 1585 1586 obj.run() 1587 fn = obj.__class__.__name__ + '.png' 1588 fp = str(environLocal.getRootTempDir() / fn) 1589 environLocal.printDebug(['writing fp:', fp]) 1590 obj.write(fp) 1591 1592 1593class Test(unittest.TestCase): 1594 1595 def testCopyAndDeepcopy(self): 1596 ''' 1597 Test copying all objects defined in this module 1598 ''' 1599 import copy 1600 import sys 1601 import types 1602 for part in sys.modules[self.__module__].__dict__: 1603 match = False 1604 for skip in ['_', '__', 'Test', 'Exception']: 1605 if part.startswith(skip) or part.endswith(skip): 1606 match = True 1607 if match: 1608 continue 1609 name = getattr(sys.modules[self.__module__], part) 1610 # noinspection PyTypeChecker 1611 if callable(name) and not isinstance(name, types.FunctionType): 1612 try: # see if obj can be made w/ args 1613 obj = name() 1614 except TypeError: 1615 continue 1616 unused_a = copy.copy(obj) 1617 unused_b = copy.deepcopy(obj) 1618 1619 def testPitchSpaceDurationCount(self): 1620 a = corpus.parse('bach/bwv57.8') 1621 b = ScatterWeightedPitchSpaceQuarterLength(a.parts[0].flatten(), doneAction=None, 1622 title='Bach (soprano voice)') 1623 b.run() 1624 1625 def testPitchSpace(self): 1626 a = corpus.parse('bach') 1627 b = HistogramPitchSpace(a.parts[0].flatten(), doneAction=None, title='Bach (soprano voice)') 1628 b.run() 1629 1630 def testPitchClass(self): 1631 a = corpus.parse('bach/bwv57.8') 1632 b = HistogramPitchClass(a.parts[0].flatten(), 1633 doneAction=None, 1634 title='Bach (soprano voice)') 1635 b.run() 1636 1637 def testQuarterLength(self): 1638 a = corpus.parse('bach/bwv57.8') 1639 b = HistogramQuarterLength(a.parts[0].flatten(), 1640 doneAction=None, 1641 title='Bach (soprano voice)') 1642 b.run() 1643 1644 def testPitchDuration(self): 1645 a = corpus.parse('schoenberg/opus19', 2) 1646 b = ScatterPitchSpaceDynamicSymbol(a.parts[0].flatten(), 1647 doneAction=None, 1648 title='Schoenberg (piano)') 1649 b.run() 1650 1651 b = ScatterWeightedPitchSpaceDynamicSymbol(a.parts[0].flatten(), 1652 doneAction=None, 1653 title='Schoenberg (piano)') 1654 b.run() 1655 1656 def testWindowed(self, doneAction=None): 1657 a = corpus.parse('bach/bwv66.6') 1658 fn = 'bach/bwv66.6' 1659 windowStep = 20 # set high to be fast 1660 1661# b = WindowedAmbitus(a.parts, title='Bach Ambitus', 1662# minWindow=1, maxWindow=8, windowStep=3, 1663# doneAction=doneAction) 1664# b.run() 1665 1666 b = WindowedKey(a.flatten(), title=fn, 1667 minWindow=1, windowStep=windowStep, 1668 doneAction=doneAction, dpi=300) 1669 b.run() 1670 self.assertEqual(b.graphLegend.data, 1671 [ 1672 ['Major', 1673 [('C#', '#f0727a'), ('D', '#ffd752'), ('E', '#eeff9a'), 1674 ('F#', '#b9f0ff'), ('A', '#bb9aff'), ('B', '#ffb5ff') 1675 ] 1676 ], 1677 ['Minor', 1678 [('c#', '#8c0e16'), ('', '#ffffff'), ('', '#ffffff'), 1679 ('f#', '#558caa'), ('', '#ffffff'), ('b', '#9b519b') 1680 ] 1681 ] 1682 ] 1683 ) 1684 1685 def testFeatures(self): 1686 streamList = ['bach/bwv66.6', 'schoenberg/opus19/movement2', 'corelli/opus3no1/1grave'] 1687 feList = ['ql1', 'ql2', 'ql3'] 1688 1689 p = Features(streamList, featureExtractors=feList, doneAction=None) 1690 p.run() 1691 1692 def testPianoRollFromOpus(self): 1693 o = corpus.parse('josquin/laDeplorationDeLaMorteDeJohannesOckeghem') 1694 s = o.mergeScores() 1695 1696 b = HorizontalBarPitchClassOffset(s, doneAction=None) 1697 b.run() 1698 1699 def testChordsA(self): 1700 from music21 import scale 1701 sc = scale.MajorScale('c4') 1702 1703 b = Histogram(stream.Stream(), doneAction=None) 1704 c = chord.Chord(['b', 'c', 'd']) 1705 b.axisX = axis.PitchSpaceAxis(b, 'x') # pylint: disable=attribute-defined-outside-init 1706 self.assertEqual(b.extractChordDataOneAxis(b.axisX, c, {}), [71, 60, 62]) 1707 1708 s = stream.Stream() 1709 s.append(chord.Chord(['b', 'c#', 'd'])) 1710 s.append(note.Note('c3')) 1711 s.append(note.Note('c5')) 1712 b = HistogramPitchSpace(s, doneAction=None) 1713 b.run() 1714 1715 # b.write() 1716 self.assertEqual(b.data, [(1, 1, {}), (2, 1, {}), (3, 1, {}), (4, 1, {}), (5, 1, {})]) 1717 1718 s = stream.Stream() 1719 s.append(sc.getChord('e3', 'a3')) 1720 s.append(note.Note('c3')) 1721 s.append(note.Note('c3')) 1722 b = HistogramPitchClass(s, doneAction=None) 1723 b.run() 1724 1725 # b.write() 1726 self.assertEqual(b.data, [(1, 2, {}), (2, 1, {}), (3, 1, {}), (4, 1, {}), (5, 1, {})]) 1727 1728 s = stream.Stream() 1729 s.append(sc.getChord('e3', 'a3', quarterLength=2)) 1730 s.append(note.Note('c3', quarterLength=0.5)) 1731 b = HistogramQuarterLength(s, doneAction=None) 1732 b.run() 1733 1734 # b.write() 1735 self.assertEqual(b.data, [(1, 1, {}), (2, 1, {})]) 1736 1737 # test scatter plots 1738 1739 b = Scatter(stream.Stream(), doneAction=None) 1740 b.axisX = axis.PitchSpaceAxis(b, 'x') # pylint: disable=attribute-defined-outside-init 1741 b.axisY = axis.QuarterLengthAxis(b, 'y') # pylint: disable=attribute-defined-outside-init 1742 b.axisY.useLogScale = False 1743 c = chord.Chord(['b', 'c', 'd'], quarterLength=0.5) 1744 1745 self.assertEqual(b.extractChordDataMultiAxis(c, {}), 1746 [[71, 60, 62], [0.5, 0.5, 0.5]]) 1747 1748 b.matchPitchCountForChords = False 1749 self.assertEqual(b.extractChordDataMultiAxis(c, {}), [[71, 60, 62], [0.5]]) 1750 # matching the number of pitches for each data point may be needed 1751 1752 def testChordsA2(self): 1753 from music21 import scale 1754 sc = scale.MajorScale('c4') 1755 1756 s = stream.Stream() 1757 s.append(sc.getChord('e3', 'a3', quarterLength=0.5)) 1758 s.append(sc.getChord('b3', 'c5', quarterLength=1.5)) 1759 s.append(note.Note('c3', quarterLength=2)) 1760 b = ScatterPitchSpaceQuarterLength(s, doneAction=None) 1761 b.axisX.useLogScale = False 1762 b.run() 1763 1764 match = [(0.5, 52.0, {}), (0.5, 53.0, {}), (0.5, 55.0, {}), (0.5, 57.0, {}), 1765 (1.5, 59.0, {}), (1.5, 60.0, {}), 1766 (1.5, 62.0, {}), (1.5, 64.0, {}), 1767 (1.5, 65.0, {}), (1.5, 67.0, {}), 1768 (1.5, 69.0, {}), (1.5, 71.0, {}), (1.5, 72.0, {}), 1769 (2.0, 48.0, {})] 1770 self.assertEqual(b.data, match) 1771 # b.write() 1772 1773 def testChordsA3(self): 1774 from music21 import scale 1775 sc = scale.MajorScale('c4') 1776 1777 s = stream.Stream() 1778 s.append(sc.getChord('e3', 'a3', quarterLength=0.5)) 1779 s.append(sc.getChord('b3', 'c5', quarterLength=1.5)) 1780 s.append(note.Note('c3', quarterLength=2)) 1781 b = ScatterPitchClassQuarterLength(s, doneAction=None) 1782 b.axisX.useLogScale = False 1783 b.run() 1784 1785 match = [(0.5, 4, {}), (0.5, 5, {}), (0.5, 7, {}), (0.5, 9, {}), 1786 (1.5, 11, {}), (1.5, 0, {}), (1.5, 2, {}), (1.5, 4, {}), (1.5, 5, {}), 1787 (1.5, 7, {}), (1.5, 9, {}), (1.5, 11, {}), (1.5, 0, {}), 1788 (2.0, 0, {})] 1789 self.assertEqual(b.data, match) 1790 # b.write() 1791 1792 def testChordsA4(self): 1793 from music21 import scale 1794 sc = scale.MajorScale('c4') 1795 1796 s = stream.Stream() 1797 s.append(sc.getChord('e3', 'a3', quarterLength=0.5)) 1798 s.append(note.Note('c3', quarterLength=2)) 1799 s.append(sc.getChord('b3', 'e4', quarterLength=1.5)) 1800 s.append(note.Note('d3', quarterLength=2)) 1801 self.assertEqual([e.offset for e in s], [0.0, 0.5, 2.5, 4.0]) 1802 1803 # s.show() 1804 b = ScatterPitchClassOffset(s, doneAction=None) 1805 b.run() 1806 1807 match = [(0.0, 4, {}), (0.0, 5, {}), (0.0, 7, {}), (0.0, 9, {}), 1808 (0.5, 0, {}), 1809 (2.5, 11, {}), (2.5, 0, {}), (2.5, 2, {}), (2.5, 4, {}), 1810 (4.0, 2, {})] 1811 self.assertEqual(b.data, match) 1812 # b.write() 1813 1814 def testChordsA5(self): 1815 from music21 import scale 1816 sc = scale.MajorScale('c4') 1817 1818 s = stream.Stream() 1819 s.append(dynamics.Dynamic('f')) 1820 s.append(sc.getChord('e3', 'a3', quarterLength=0.5)) 1821 # s.append(note.Note('c3', quarterLength=2)) 1822 s.append(dynamics.Dynamic('p')) 1823 s.append(sc.getChord('b3', 'e4', quarterLength=1.5)) 1824 # s.append(note.Note('d3', quarterLength=2)) 1825 1826 # s.show() 1827 b = ScatterPitchSpaceDynamicSymbol(s, doneAction=None) 1828 b.run() 1829 1830 self.assertEqual(b.data, [(52, 8, {}), (53, 8, {}), (55, 8, {}), 1831 (57, 8, {}), (59, 8, {}), (59, 5, {}), 1832 (60, 8, {}), (60, 5, {}), (62, 8, {}), 1833 (62, 5, {}), (64, 8, {}), (64, 5, {})]) 1834 # b.write() 1835 1836 def testChordsB(self): 1837 from music21 import scale 1838 sc = scale.MajorScale('c4') 1839 1840 s = stream.Stream() 1841 s.append(note.Note('c3')) 1842 s.append(sc.getChord('e3', 'a3', quarterLength=0.5)) 1843 # s.append(note.Note('c3', quarterLength=2)) 1844 s.append(sc.getChord('b3', 'e4', quarterLength=1.5)) 1845 1846 b = HorizontalBarPitchClassOffset(s, doneAction=None) 1847 b.run() 1848 1849 match = [['C', [(0.0, 0.9375), (1.5, 1.4375)], {}], 1850 ['', [], {}], 1851 ['D', [(1.5, 1.4375)], {}], 1852 ['', [], {}], 1853 ['E', [(1.0, 0.4375), (1.5, 1.4375)], {}], 1854 ['F', [(1.0, 0.4375)], {}], 1855 ['', [], {}], 1856 ['G', [(1.0, 0.4375)], {}], 1857 ['', [], {}], 1858 ['A', [(1.0, 0.4375)], {}], 1859 ['', [], {}], 1860 ['B', [(1.5, 1.4375)], {}]] 1861 self.assertEqual(b.data, match) 1862 # b.write() 1863 1864 s = stream.Stream() 1865 s.append(note.Note('c3')) 1866 s.append(sc.getChord('e3', 'a3', quarterLength=0.5)) 1867 # s.append(note.Note('c3', quarterLength=2)) 1868 s.append(sc.getChord('b3', 'e4', quarterLength=1.5)) 1869 1870 b = HorizontalBarPitchSpaceOffset(s, doneAction=None) 1871 b.run() 1872 match = [['C3', [(0.0, 0.9375)], {}], 1873 ['', [], {}], 1874 ['', [], {}], 1875 ['', [], {}], 1876 ['E', [(1.0, 0.4375)], {}], 1877 ['F', [(1.0, 0.4375)], {}], 1878 ['', [], {}], 1879 ['G', [(1.0, 0.4375)], {}], 1880 ['', [], {}], 1881 ['A', [(1.0, 0.4375)], {}], 1882 ['', [], {}], 1883 ['B', [(1.5, 1.4375)], {}], 1884 ['C4', [(1.5, 1.4375)], {}], 1885 ['', [], {}], 1886 ['D', [(1.5, 1.4375)], {}], 1887 ['', [], {}], 1888 ['E', [(1.5, 1.4375)], {}]] 1889 1890 self.assertEqual(b.data, match) 1891 # b.write() 1892 1893 s = stream.Stream() 1894 s.append(note.Note('c3')) 1895 s.append(sc.getChord('e3', 'a3', quarterLength=0.5)) 1896 # s.append(note.Note('c3', quarterLength=2)) 1897 s.append(sc.getChord('b3', 'e4', quarterLength=1.5)) 1898 s.append(sc.getChord('f4', 'g5', quarterLength=3)) 1899 s.append(sc.getChord('f4', 'g5', quarterLength=3)) 1900 s.append(note.Note('c5', quarterLength=3)) 1901 1902 b = ScatterWeightedPitchSpaceQuarterLength(s, doneAction=None) 1903 b.axisX.useLogScale = False 1904 b.run() 1905 1906 self.assertEqual(b.data[0:7], [(0.5, 52.0, 1, {}), (0.5, 53.0, 1, {}), (0.5, 55.0, 1, {}), 1907 (0.5, 57.0, 1, {}), (1.0, 48.0, 1, {}), (1.5, 59.0, 1, {}), 1908 (1.5, 60.0, 1, {})]) 1909 # b.write() 1910 1911 s = stream.Stream() 1912 s.append(note.Note('c3')) 1913 s.append(sc.getChord('e3', 'a3', quarterLength=0.5)) 1914 # s.append(note.Note('c3', quarterLength=2)) 1915 s.append(sc.getChord('b3', 'e4', quarterLength=1.5)) 1916 s.append(sc.getChord('f4', 'g5', quarterLength=3)) 1917 s.append(sc.getChord('f4', 'g5', quarterLength=3)) 1918 s.append(note.Note('c5', quarterLength=3)) 1919 1920 b = ScatterWeightedPitchClassQuarterLength(s, doneAction=None) 1921 b.axisX.useLogScale = False 1922 b.run() 1923 1924 self.assertEqual(b.data[0:8], [(0.5, 4, 1, {}), (0.5, 5, 1, {}), (0.5, 7, 1, {}), 1925 (0.5, 9, 1, {}), 1926 (1.0, 0, 1, {}), 1927 (1.5, 0, 1, {}), (1.5, 2, 1, {}), (1.5, 4, 1, {})]) 1928 # b.write() 1929 1930 def testChordsB2(self): 1931 from music21 import scale 1932 sc = scale.MajorScale('c4') 1933 1934 s = stream.Stream() 1935 s.append(dynamics.Dynamic('f')) 1936 # s.append(note.Note('c3')) 1937 c = sc.getChord('e3', 'a3', quarterLength=0.5) 1938 self.assertEqual(repr(c), '<music21.chord.Chord E3 F3 G3 A3>') 1939 self.assertEqual([n.pitch.ps for n in c], [52.0, 53.0, 55.0, 57.0]) 1940 s.append(c) 1941 # s.append(note.Note('c3', quarterLength=2)) 1942 s.append(dynamics.Dynamic('mf')) 1943 s.append(sc.getChord('b3', 'e4', quarterLength=1.5)) 1944 s.append(dynamics.Dynamic('pp')) 1945 s.append(sc.getChord('f4', 'g5', quarterLength=3)) 1946 s.append(sc.getChord('f4', 'g5', quarterLength=3)) 1947 s.append(note.Note('c5', quarterLength=3)) 1948 1949 b = ScatterWeightedPitchSpaceDynamicSymbol(s, doneAction=None) 1950 b.axisX.useLogScale = False 1951 b.run() 1952 match = [(52.0, 8, 1, {}), (53.0, 8, 1, {}), (55.0, 8, 1, {}), (57.0, 8, 1, {}), 1953 (59.0, 7, 1, {}), (59.0, 8, 1, {}), (60.0, 7, 1, {}), (60.0, 8, 1, {}), 1954 (62.0, 7, 1, {}), (62.0, 8, 1, {}), (64.0, 7, 1, {}), (64.0, 8, 1, {}), 1955 (65.0, 4, 2, {}), (65.0, 7, 1, {}), 1956 (67.0, 4, 2, {}), (67.0, 7, 1, {}), 1957 (69.0, 4, 2, {}), (69.0, 7, 1, {}), (71.0, 4, 2, {}), (71.0, 7, 1, {}), 1958 (72.0, 4, 3, {}), (72.0, 7, 1, {}), (74.0, 4, 2, {}), (74.0, 7, 1, {}), 1959 (76.0, 4, 2, {}), (76.0, 7, 1, {}), (77.0, 4, 2, {}), (77.0, 7, 1, {}), 1960 (79.0, 4, 2, {}), (79.0, 7, 1, {})] 1961 1962 self.maxDiff = 2048 1963 # TODO: Is this right? why are the old dynamics still active? 1964 self.assertEqual(b.data, match) 1965 # b.write() 1966 1967 def testChordsB3(self): 1968 from music21 import scale 1969 sc = scale.MajorScale('c4') 1970 1971 s = stream.Stream() 1972 s.append(dynamics.Dynamic('f')) 1973 s.append(note.Note('c3')) 1974 s.append(sc.getChord('e3', 'a3', quarterLength=0.5)) 1975 s.append(dynamics.Dynamic('mf')) 1976 s.append(sc.getChord('b3', 'e4', quarterLength=1.5)) 1977 s.append(dynamics.Dynamic('pp')) 1978 s.append(sc.getChord('f4', 'g5', quarterLength=3)) 1979 s.append(note.Note('c5', quarterLength=3)) 1980 1981 b = Plot3DBarsPitchSpaceQuarterLength(s, doneAction=None) 1982 b.axisX.useLogScale = False 1983 b.run() 1984 1985 self.assertEqual(b.data[0], (0.5, 52.0, 1, {})) 1986 # b.write() 1987 1988 def testDolanA(self): 1989 a = corpus.parse('bach/bwv57.8') 1990 b = Dolan(a, title='Bach', doneAction=None) 1991 b.run() 1992 1993 # b.show() 1994 1995 1996# ------------------------------------------------------------------------------ 1997# define presented order in documentation 1998_DOC_ORDER = [ 1999 HistogramPitchSpace, 2000 HistogramPitchClass, 2001 HistogramQuarterLength, 2002 # windowed 2003 WindowedKey, 2004 WindowedAmbitus, 2005 # scatters 2006 ScatterPitchSpaceQuarterLength, 2007 ScatterPitchClassQuarterLength, 2008 ScatterPitchClassOffset, 2009 ScatterPitchSpaceDynamicSymbol, 2010 # offset based horizontal 2011 HorizontalBarPitchSpaceOffset, 2012 HorizontalBarPitchClassOffset, 2013 Dolan, 2014 # weighted scatter 2015 ScatterWeightedPitchSpaceQuarterLength, 2016 ScatterWeightedPitchClassQuarterLength, 2017 ScatterWeightedPitchSpaceDynamicSymbol, 2018 # 3d graphs 2019 Plot3DBarsPitchSpaceQuarterLength, 2020] 2021 2022 2023if __name__ == '__main__': 2024 import music21 2025 music21.mainTest(TestExternalManual) # , runTest='test3DPitchSpaceQuarterLengthCount') 2026