# -*- coding: ISO-8859-1 -*- """ capellaScript -- >>> Note Head Names This applies the pitch name of each note to the center of the note head. Accidentals are represented by coloring the outer ring of each note head with the desired color. This changes the notation font to capella-kids which presents a round note head body, giving more area in which to display the note names. Allows saving color choices. Author: Ken Knollman ken@gara.us Cincinnati, Ohio, USA. <<< """ title ='Note Head Names - Version : ' version = '1.120 11/20/2023' helpText1 = 'This applies the pitch name of each note to the center of the note head. ' helpText2 = 'Accidentals are represented by coloring the outer ring of each note ' helpText3 = 'head with the desired color. Note that you cannot edit the score after ' helpText4 = 'Note Head Names have been applied. To edit the score, first remove ' helpText5 = 'Note Head Names, edit the score, then re-apply Note Head Names. ' helpText6 = 'You may save your color selections for future use of Note Head Names.' from xml.dom.minidom import parseString, NodeList, Node, Element from caplib.rational import Rational from caplib.capDOM import ScoreChange import tempfile, string, new, os nhnRemovalTag = '132474-1001' doc = parseString('') nameFont = 'Verdana' nameCharSet = '1' nameFontWeight = '900' nameFontHeight = '3.8' def openingDialog(): #Present the opening dialog screen to the user colorValueList = [('FF0000'),# red ('FF8000'),# orange ('FFFF00'),# yellow ('00FF00'),# green ('7FDFD1'),# cyan ('0000FF'),# blue ('6000A0'),# violet ('000000'),# black ('808080'),# dark gray ('C0C0C0'),# light gray ('FFFFFF')]# white colorNameList = [('red'), ('orange'), ('yellow'), ('green'), ('cyan'), ('blue'), ('violet'), ('black'), ('dark gray'), ('light gray'), ('white')] global noteDarkColor, noteLightColor, noteRingFlatColor, noteRingSharpColor, noteRingDblFlatColor, noteRingDblSharpColor, selectedAction, saveSelections if os.path.exists(getPersonalDataDir() + "nhnColorSelections.cfg"): colorSelectionsFile = open(getPersonalDataDir() + "nhnColorSelections.cfg", 'r') noteDarkColor = colorSelectionsFile.readline().strip('\n') noteLightColor = colorSelectionsFile.readline().strip('\n') noteRingFlatColor = colorSelectionsFile.readline().strip('\n') noteRingSharpColor = colorSelectionsFile.readline().strip('\n') noteRingDblFlatColor = colorSelectionsFile.readline().strip('\n') noteRingDblSharpColor = colorSelectionsFile.readline().strip('\n') colorSelectionsFile.close() else: noteDarkColor = '0000FF' noteLightColor = 'FFFFFF' noteRingFlatColor = 'FF0000' noteRingSharpColor = '00FF00' noteRingDblFlatColor = 'FF8000' noteRingDblSharpColor = '6000A0' selectedAction = 0 space = Label('') noteDarkColorSel = ComboBox(colorNameList, width = 10, value = colorValueList.index(noteDarkColor)) noteLightColorSel = ComboBox(colorNameList, width = 10, value = colorValueList.index(noteLightColor)) noteRingFlatColorSel = ComboBox(colorNameList, width = 10, value = colorValueList.index(noteRingFlatColor)) noteRingSharpColorSel = ComboBox(colorNameList, width = 10, value = colorValueList.index(noteRingSharpColor)) noteRingDblFlatColorSel = ComboBox(colorNameList, width = 10, value = colorValueList.index(noteRingDblFlatColor)) noteRingDblSharpColorSel = ComboBox(colorNameList, width = 10, value = colorValueList.index(noteRingDblSharpColor)) actionSelector = Radio(['Apply Note Names', 'Remove Note Names'], text = 'Action', value = 0) saveSelector = CheckBox('Save Color Selections', value = 0) noteDarkBox = HBox([Label('Note Dark', width = 10), noteDarkColorSel, space]) noteLightBox = HBox([Label('Note Light', width = 10), noteLightColorSel, space]) flatRingBox = HBox([Label('Flat Ring', width = 10), noteRingFlatColorSel, space]) sharpRingBox = HBox([Label('Sharp Ring', width = 10), noteRingSharpColorSel, space]) dblFlatRingBox = HBox([Label('Dbl Flat Ring', width = 10), noteRingDblFlatColorSel, space]) dblSharpRingBox = HBox([Label('Dbl Sharp Ring', width = 10), noteRingDblSharpColorSel, space]) colorBox = VBox([noteDarkBox, noteLightBox, flatRingBox, sharpRingBox, dblFlatRingBox, dblSharpRingBox], text = 'Color Selector', padding = 10) noteBox = VBox([Label(' '), Label(helpText1), Label(helpText2), Label(helpText3), Label(helpText4), Label(helpText5), Label(helpText6)]) saveBox = HBox([space, saveSelector]) innerBox = VBox([actionSelector, saveBox, noteBox]) dlg = Dialog(title + version, HBox([colorBox, innerBox, space], padding=5)) if dlg.run(): noteDarkColor = colorValueList[noteDarkColorSel.value()] noteLightColor = colorValueList[noteLightColorSel.value()] noteRingFlatColor = colorValueList[noteRingFlatColorSel.value()] noteRingSharpColor = colorValueList[noteRingSharpColorSel.value()] noteRingDblFlatColor = colorValueList[noteRingDblFlatColorSel.value()] noteRingDblSharpColor = colorValueList[noteRingDblSharpColorSel.value()] if saveSelector.value() == 1: colorSelectionsFile = open(getPersonalDataDir() + "nhnColorSelections.cfg", "w") colorSelectionsFile.write(noteDarkColor + '\n') colorSelectionsFile.write(noteLightColor + '\n') colorSelectionsFile.write(noteRingFlatColor + '\n') colorSelectionsFile.write(noteRingSharpColor + '\n') colorSelectionsFile.write(noteRingDblFlatColor + '\n') colorSelectionsFile.write(noteRingDblSharpColor + '\n') colorSelectionsFile.close() selectedAction = actionSelector.value() return True else: return False def removeNoteHeadNames(score): #Only remove the nodes this script created to begin with. To do this, we are #looking for elements only within the chord nodes that have removal tags we created. for chord in score.getElementsByTagName('chord'): for display in chord.getElementsByTagName('display'): if display.hasAttribute('color'): display.parentNode.removeChild(display) for drawObj in chord.getElementsByTagName('drawObj'): for basic in drawObj.getElementsByTagName('basic'): if basic.hasAttribute('tag') and basic.getAttribute('tag') == nhnRemovalTag: drawObj.parentNode.removeChild(drawObj) def removeStyle(score): for layout in score.getElementsByTagName('layout'): for style in layout.getElementsByTagName('style'): style.parentNode.removeChild(style) def createDefaultStyle(score): #Set up default score style (Capella Standard) fontFaceName = 'capella3' removeStyle(score) #This all comes from Capella Standard XML, reproduced here for layout in score.getElementsByTagName('layout'): style = layout.gotoOrCreateChild('style') style.setAttribute('fontFace', fontFaceName) noteSpaces = style.gotoOrCreateChild('noteSpaces') noteSpaces.setAttribute('rightBarline', '1.34375') noteSpaces.setAttribute('leftBarline', '0.9375') noteSpaces.setAttribute('firstNote', '2') widths = style.gotoOrCreateChild('widths') widths.setAttribute('noteLine', '0.08203125') widths.setAttribute('head4', '1.19921875') widths.setAttribute('head2', '1.19921875') widths.setAttribute('head1', '1.6640625') widths.setAttribute('head0', '1.984375') widths.setAttribute('stem', '0.09375') widths.setAttribute('legerLineJut', '0.0390625') widths.setAttribute('barline', '0.08203125') stemTouch = style.gotoOrCreateChild('stemTouch') stemTouch.setAttribute('xUp2', '1.19921875') stemTouch.setAttribute('yDown4', '-0.16796875') stemTouch.setAttribute('yUp2', '0.16796875') stemTouch.setAttribute('yUp4', '0.16796875') stemTouch.setAttribute('xDown2', '0.00') stemTouch.setAttribute('yDown2', '-0.16796875') stemTouch.setAttribute('xDown4', '0.00') stemTouch.setAttribute('xUp4', '1.19921875') bracketShape = style.gotoOrCreateChild('bracketShape') bracketShape.setAttribute('y2', '-0.5') bracketShape.setAttribute('y6', '-0.125') bracketShape.setAttribute('x6', '0.25') bracketShape.setAttribute('hasEnds', 'true') bracketShape.setAttribute('x7', '-0.75') bracketShape.setAttribute('x2', '0.25') bracketShape.setAttribute('width', '0.25') bracketShape.setAttribute('y7', '-0.125') bracketShape.setAttribute('dx', '-0.5') bracketShape.setAttribute('dy', '-0.5') bracketShape.setAttribute('y1', '-0.5') bracketShape.setAttribute('x3', '0.9375') bracketShape.setAttribute('y3', '-1') bracketShape.setAttribute('x1', '-0.75') bracketShape.setAttribute('y5', '-1') bracketShape.setAttribute('y4', '-1.5') bracketShape.setAttribute('x4', '1.25') bracketShape.setAttribute('x5', '0.9375') slurShape = style.gotoOrCreateChild('slurShape') slurShape.setAttribute('mayMoveTowardsHead', 'true') def createNoteHeadStyle(score): #Set up round head score style (Capella Kids) fontFaceName = 'capella-Früherziehung'.decode('utf8') removeStyle(score) #This all comes from Capella Kids XML, reproduced here for layout in score.getElementsByTagName('layout'): style = layout.gotoOrCreateChild('style') style.setAttribute('fontFace', fontFaceName) noteSpaces = style.gotoOrCreateChild('noteSpaces') noteSpaces.setAttribute('rightBarline', '1.34375') noteSpaces.setAttribute('leftBarline', '0.9375') noteSpaces.setAttribute('firstNote', '2') widths = style.gotoOrCreateChild('widths') widths.setAttribute('noteLine', '0.1171875') widths.setAttribute('head4', '1.007812') widths.setAttribute('head2', '1.0078125') widths.setAttribute('head1', '1.0078125') widths.setAttribute('head0', '1.09375') widths.setAttribute('stem', '0.140625') widths.setAttribute('legerLineJut', '0.0390625') widths.setAttribute('barline', '0.09375') stemTouch = style.gotoOrCreateChild('stemTouch') stemTouch.setAttribute('xUp2', '1.015625') stemTouch.setAttribute('yDown4', '0.00') stemTouch.setAttribute('yUp2', '0.00') stemTouch.setAttribute('yUp4', '0.00') stemTouch.setAttribute('xDown2', '0.00') stemTouch.setAttribute('yDown2', '0.00') stemTouch.setAttribute('xDown4', '0.00') stemTouch.setAttribute('xUp4', '1.015625') bracketShape = style.gotoOrCreateChild('bracketShape') bracketShape.setAttribute('y2', '-0.5') bracketShape.setAttribute('y6', '-0.125') bracketShape.setAttribute('x6', '0.25') bracketShape.setAttribute('hasEnds', 'true') bracketShape.setAttribute('x7', '-0.75') bracketShape.setAttribute('x2', '0.25') bracketShape.setAttribute('width', '0.25') bracketShape.setAttribute('y7', '-0.125') bracketShape.setAttribute('dx', '-0.5') bracketShape.setAttribute('dy', '-0.5') bracketShape.setAttribute('y1', '-0.5') bracketShape.setAttribute('x3', '0.9375') bracketShape.setAttribute('y3', '-1') bracketShape.setAttribute('x1', '-0.75') bracketShape.setAttribute('y5', '-1') bracketShape.setAttribute('y4', '-1.5') bracketShape.setAttribute('x4', '1.25') bracketShape.setAttribute('x5', '0.9375') slurShape = style.gotoOrCreateChild('slurShape') slurShape.setAttribute('mayMoveTowardsHead', 'true') def gotoOrCreateChild(self, name, newChild=False): #newChild=true means create a new child node with the same name as existing child nodes under #the same parent. Otherwise, use the same node if it exists or create one if it doesn't. newElement = None if newChild: pass else: for child in self.childNodes: if child.nodeType == child.ELEMENT_NODE and child.tagName == name: newElement = child break if newElement == None: newElement = doc.createElement(name) self.appendChild(newElement) return newElement Node.gotoOrCreateChild = new.instancemethod(gotoOrCreateChild,None,Node) def getNoteOrdinal(pitch): #The ordinal value of a note is an integer that represents the pitch and octave of the note. #The pitch consists of two characters - the note name (CDEFGAB) and its octave, eg C5. #The ordinal value is based on the lowest note in scale (C) return (ord(pitch[0]) - ord('C')) % 7 + 7 * eval(pitch[1]) def getcollisionLowNote(chord): #If two notes in a chord are colliding on a second interval #with each other, return the lower of the two colliding notes noteList = [] for head in chord.getElementsByTagName('head'): #Build a list of note ordinals inthe chord noteList.append(getNoteOrdinal(head.getAttribute('pitch'))) #If there is more than one note in the chord if len(noteList) > 1: lo = 0 #Find the low note for hi in noteList: #If the notes are a second interval apart, return the low note if hi - lo == 1: return lo lo = hi #otherwise, no collision return 0 def getStemDirection(voice, chord, staffZeroOrdinal): #Determine the direction of the stem for the specified chord #This is needed to determine which side of the stem to place #the lower of the two colliding 2nd interval notes (if there are any) #Both the voice and the chord may have stem direction attributes voiceStemDir = 'auto' if voice.hasAttribute('stemDir'): voiceStemDir = 'down' if voice.getAttribute('stemDir') == 'up': voiceStemDir = 'up' #If the chord stem direction for this chord is given and is not auto #then the chord stem direction supercedes the voice stem direction chordStemDir = 'auto' for stem in chord.getElementsByTagName('stem'): chordStemDir = stem.getAttribute('dir') if chordStemDir <> 'auto': stemDir = chordStemDir #If the chord stem direction is auto, then check to see if the voice #stem direction is not. If it is not, use the voice stem direction else: if voiceStemDir <> 'auto': stemDir = voiceStemDir #If both voice and stem directions are auto (not specified in the capex file) #then determine the stem direction from the formula below. else: noteOrdMin = 999 noteOrdMax = 0 for head in chord.getElementsByTagName('head'): pitch = head.getAttribute('pitch') noteOrdinal = getNoteOrdinal(pitch) noteOrdMin = min(noteOrdinal, noteOrdMin) noteOrdMax = max(noteOrdinal, noteOrdMax) stemDir = 'up' #Look for average of the highest and lowest notes in the chord and compare #that to the staff mid line (zero) ordinal. If equal or greater than the #mid line, the stem goes down. Otherwise it goes up. if (noteOrdMin + noteOrdMax)/2.0 >= staffZeroOrdinal: stemDir = 'down' return stemDir def getStaffZeroOrdinal(clefSign): #Get the ordinal value for the staff zero (center) staff line #The following diatonic pitches fall on the center staff line for each clef #It would appear that Capella and Wikipedia disagree on which octave the staff #mid lines fall on. Capella wins. if clefSign == 'treble': staffZeroOrdinal = getNoteOrdinal('B5') elif clefSign == 'bass': staffZeroOrdinal = getNoteOrdinal('D4') elif clefSign == 'alto': staffZeroOrdinal = getNoteOrdinal('C5') elif clefSign == 'tenor': staffZeroOrdinal = getNoteOrdinal('A4') elif clefSign == 'G2+': #Octave Treble staffZeroOrdinal = getNoteOrdinal('B5') elif clefSign == 'G2-': #SubOctave Treble staffZeroOrdinal = getNoteOrdinal('B5') if '-' in clefSign: #Down one octave staffZeroOrdinal -= 7 elif '+' in clefSign: #Up one octave staffZeroOrdinal += 7 if staffZeroOrdinal > 0: return staffZeroOrdinal else: return 0 def getNoteXlocation(noteOrdinal, collisionLowNote, chordStemDirection): #Capella determines the X location of the note internally. This just trims that #based on the factors below. noteXlocation = 0 #Set up the X location of the head if noteOrdinal == collisionLowNote and chordStemDirection == 'down': noteXlocation -= 1.1 elif noteOrdinal == collisionLowNote + 1 and chordStemDirection == 'up': noteXlocation += 1.0 return(noteXlocation) def getNoteYlocation(noteOrdinal, staffZeroOrdinal): #Capella places the Y drawing location 0 on the middle of the staff. #Determine where to place the notehead vertically on the staff #Each vertical note space is worth 1/2 of the noteOrdinal value verticalSpaceWeight = 0.5 #The center of the note name character is negatively offset from its baseline verticalCharacterOffset = 0.3 #Capella inverts the Y location passed to it, so more negative values are higher on the staff. noteYlocation = -(((noteOrdinal - staffZeroOrdinal) * verticalSpaceWeight) - verticalCharacterOffset) return(noteYlocation) def getNoteNameXtrim(pitch): if pitch == 'A': return +0.20 if pitch == 'B': return +0.24 if pitch == 'C': return +0.20 if pitch == 'D': return +0.21 if pitch == 'E': return +0.20 if pitch == 'F': return +0.22 if pitch == 'G': return +0.16 return 0 def getNoteNameYtrim(pitch): if pitch == 'A': return +0.00 if pitch == 'B': return +0.04 if pitch == 'C': return +0.00 if pitch == 'D': return +0.00 if pitch == 'E': return +0.03 if pitch == 'F': return +0.04 if pitch == 'G': return +0.00 return 0 #This is the worker routine def addNoteHeadNames(score): removeNoteHeadNames(score) clefSign = '' chordStemDirection = '' noteNum = 0 for system in score.getElementsByTagName('systems'): for staff in system.getElementsByTagName('staves'): for voice in staff.getElementsByTagName('voice'): for noteObjects in voice.getElementsByTagName('noteObjects'): for noteObj in noteObjects.childNodes: #Determine if this is a clef sign if noteObj.nodeType == noteObj.ELEMENT_NODE and noteObj.tagName == 'clefSign': clefSign = noteObj.getAttribute('clef') #Determine if this is a new chord if noteObj.nodeType == noteObj.ELEMENT_NODE and noteObj.tagName == 'chord': chord = noteObj #The original note (kids' font) is maintained. The graphical objects are drawn #on top of them to improve appearance. #Color the note and graphical note head the same for uniformity. display = doc.createElement('display') chord.appendChild(display) display.setAttribute('color', noteDarkColor) basic = chord.gotoOrCreateChild('basic') basic.setAttribute('tag', nhnRemovalTag) #Now see if there are any second interval collisions in this chord #If so, grab the low note of the collision collisionLowNote = getcollisionLowNote(chord) #Get the ordinal value of the middle line in the current staff staffZeroOrdinal = getStaffZeroOrdinal(clefSign) #Look to see if the chord stem is up or down chordStemDirection = getStemDirection(voice, chord, staffZeroOrdinal) #Look for the duration of the note noteDuration = "0" for duration in noteObj.getElementsByTagName('duration'): noteDuration = duration.getAttribute('base') #Get the note head and its pitch #Also note the note number has increased for this chord for head in chord.getElementsByTagName('head'): pitch = head.getAttribute('pitch') noteNum += 1 #Look to see if note is sharp or flat step = 0 for alter in head.getElementsByTagName('alter'): if alter: if alter.hasAttribute('step'): step = alter.getAttribute('step') #Half and Whole notes have open centers, all others filled if noteDuration == "1/2" or noteDuration == "1/1": noteColor = noteLightColor noteNameColor = noteDarkColor else: noteColor = noteDarkColor noteNameColor = noteLightColor #Set the note ring color for the accidental noteRingColor = noteDarkColor noteRingWidth = "0.15" if step == "-1": #Flat noteRingColor = noteRingFlatColor elif step == "-2": #Dbl Flat noteRingColor = noteRingDblFlatColor elif step == "1": #Sharp noteRingColor = noteRingSharpColor elif step == "2": #Dbl Sharp noteRingColor = noteRingDblSharpColor noteOrdinal = getNoteOrdinal(pitch) noteYlocation = getNoteYlocation(noteOrdinal, staffZeroOrdinal) noteXlocation = getNoteXlocation(noteOrdinal, collisionLowNote, chordStemDirection) #This is the DrawObject script that creates the note heads and names drawObjects = chord.gotoOrCreateChild('drawObjects') #First, let's draw the new round heads #The magic numbers below are trim offsets drawObj = drawObjects.gotoOrCreateChild('drawObj', True) circleHead = drawObj.gotoOrCreateChild('ellipse') circleHead.setAttribute('y2', str(noteYlocation + 0.2)) circleHead.setAttribute('lineWidth', noteRingWidth) circleHead.setAttribute('x2', str(noteXlocation + 1)) circleHead.setAttribute('fillColor', noteColor) circleHead.setAttribute('lineColor', noteRingColor) circleHead.setAttribute('y1', str(noteYlocation - 0.8)) circleHead.setAttribute('x1', str(noteXlocation)) circleHead.setAttribute('filled', 'true') basic = drawObj.gotoOrCreateChild('basic') basic.setAttribute('noAdjust', 'true') basic.setAttribute('scope', 'staff') basic.setAttribute('tag', nhnRemovalTag) #Now, let's draw the names drawObj = drawObjects.gotoOrCreateChild('drawObj', True) text = drawObj.gotoOrCreateChild('text') #Offset the X and Y positions of the notehead character #Each name character is trimmed individually. noteXlocation += getNoteNameXtrim(pitch[0]) noteYlocation += getNoteNameYtrim(pitch[0]) text.setAttribute('x', str(noteXlocation)) text.setAttribute('y', str(noteYlocation)) font = text.gotoOrCreateChild('font') font.setAttribute('face', nameFont) #Set the appropriate font height font.setAttribute('height', nameFontHeight) font.setAttribute('weight', nameFontWeight) font.setAttribute('charset', nameCharSet) font.setAttribute('color', noteNameColor) content = text.gotoOrCreateChild('content') textNode = doc.createTextNode(pitch[0]) content.appendChild(textNode) basic = drawObj.gotoOrCreateChild('basic') basic.setAttribute('tag', nhnRemovalTag) #Change the score: class ScoreChange(ScoreChange): def changeScore(self, score): if selectedAction == 0: #Apply Note Head Names. Set round style & create Note Head Names createNoteHeadStyle(score) addNoteHeadNames(score) else: #Remove Note Head Names & return to default style removeNoteHeadNames(score) createDefaultStyle(score) # ------ Main programm ------ # if activeScore(): #Get user input: if openingDialog(): global tempInput, tempOutput activeScore().registerUndo("Note Head Names") #Make the temporary files tempInput = tempfile.mktemp('.capx') tempOutput = tempfile.mktemp('.capx') activeScore().write(tempInput) #Change the score, reading the temp input file and #writing the cnanged score to the temporary output file ScoreChange(tempInput, tempOutput) #Now read the newly changed file into Change activeScore().read(tempOutput) #Delete the temporary files os.remove(tempInput) os.remove(tempOutput)