# -*- coding: ISO-8859-1 -*- """ capellaScript -- Copyright (c) 2004 Hartmut Ring >>> Transponierbares Akkordsymbol Über dem Akkord an der Cursorposition ein Akkordsymbol einfügen.|| Das Symbol wird in einem Dialog in ein Textfeld eingetippt und automatisch formatiert und transponierbar gemacht. <<< Fehlerkorrekturen (Paul Villiger, mit vlg. markiert): (1) Bei Akkordsymbolen mit z.B. G/F werden nicht alle Intervalle richtig berechnet. (2) Bei Akkordsymbolen mit # oder b am Anfang des Textes wird ein leeres Textelement eingefügt, was bei externen Skripts falsch umgesetzt und mit Inhalt gefüllt wird. """ helpText = """Geben Sie die Akkordbezeichnung ohne Rücksicht auf die Formatierung in das Textfeld ein. Am Anfang muss ein Akkord stehen: Buchstabe A bis G, danach kann # oder b und evtl. ein m stehen). Der Rest (falls vorhanden) muss durch ein Leerzeichen abgetrennt werden. Es wird ein (mit automatischer Hoch- und Tiefstellung) passend formatiertes transponierbares Akkordsymbol an der Cursorposition eingefügt! Beispiele für die Eingabe: C \t(C-Dur) Bb \t(B-Dur) F#m \t(fis-Moll) Cm 7b5 \t(c-Moll mit kl. Sept., verm. Quinte) F# maj7\t(Fis-Dur mit großer Septime) C/E \t(C-Dur mit E im Bass) Csus \t(C-Dur oder -Moll mit Quartvorhalt)""" class ChordSymbol(object): def __init__(self, obj, s, y=3.5, relSize=1.0, sansSerif=False, Runter=True, KleinS=True): self.s = s self.x = 0 self.relSize = relSize self.factorSmall = 0.6 normalPitch = 10.0 * relSize slashPitch = 18.0 * relSize self.capPitch = 12.0 * relSize self.yBase = -y # Basislinie: normale Schrift self.dyBaseSharp = -0.8 * relSize self.dyBaseBemol = -0.4 * relSize self.obj = obj if Runter: self.yBase = -y # Basislinie: normale Schrift else: y = y + 1.2 self.yBase = +y # Basislinie: normale Schrift self.yHigh = self.yBase - 1.0 * relSize # Basislinie: hochgestellt self.ySlash = self.yBase + 1.0 * relSize # Basislinie: Schrägstrich self.yLow = self.yBase + 1.0 * relSize # Basislinie: tief (nach Schrägstrich) if sansSerif: self.normalFace = 'Arial' else: self.normalFace = 'Times New Roman' self.KS = KleinS self.normalFont = dict(face=self.normalFace, height=normalPitch) self.smallFont = dict(face=self.normalFace, height=self.factorSmall*normalPitch) self.slashFont = dict(face=self.normalFace, height=slashPitch) if KleinS: self.notes = ['fb', 'cb', 'gb', 'db', 'ab', 'eb', 'bb', 'f', 'c', 'g', 'd', 'a', 'e', 'h', 'f#', 'c#', 'g#', 'd#', 'a#', 'e#', 'b#'] else: self.notes = ['Fb', 'Cb', 'Gb', 'Db', 'Ab', 'Eb', 'Bb', 'F', 'C', 'G', 'D', 'A', 'E', 'H', 'F#', 'C#', 'G#', 'D#', 'A#', 'E#', 'B#'] def appendAlteration(self, y, sharp=True, small=False): """ Behandlung der Alterationssymbole # und b mit dem capella-Font. Hilfsmethode für appendString() """ dx = 0.4*self.relSize if sharp: dy = self.dyBaseSharp symb = 'S' # das Symbol # liegt im capella-Font bei S else: dy = self.dyBaseBemol symb = 'Q' # das Symbol b liegt im capella-Font bei Q height = self.capPitch if small: height *= self.factorSmall dy *= self.factorSmall dx *= 2 # damit näher an der rechten Ziffer capFont = dict(face = 'capella3', pitchAndFamily = 2, # FF_DONTCARE | VARIABLE_PITCH charSet = 2, # SYMBOL_CHARSET height = height) t = dict(type='text', x=self.x+dx, y=y+dy, content=symb, font=capFont) # Die Breite der capella-Zeichen enthält zu viel Leerraum. # Deshalb Breitenbestimmung mit einem passenden Buchstaben ('-'): if small: test = dict(type='text', x=self.x, y=0, content='c', font=self.smallFont) else: test = dict(type='text', x=self.x, y=0, content='c', font=self.normalFont) size = self.obj.textSize(test) self.x += size[0] self.groupItems.append(t) def appendText(self, s, y, small=False): """ Behandlung von einfachem Text, Hilfsmethode für appendString() """ font = self.normalFont if small: font = self.smallFont o = dict(type='text', x=self.x, y=y, content=s, font=font) size = self.obj.textSize(o) self.x += size[0] self.groupItems.append(o) def appendString(self, s, y, small=False): """ Da das Akkordsymbol aus unterschiedlichen Fonts (Text/capella) besteht, wird es als Gruppe aus verschiedenen Einfachtexten zusammengesetzt Hilfsmethode für createSymbol() """ if s[0:2] == 'bb': self.appendText(s[0:1], y, small) self.appendAlteration(y, s[1:2] == '#', small) if len(s) > 2: self.appendText(s[2:], y, small) else: while max(s.find('b'),s.find('#')) >= 0: i = max(s.find('b'),s.find('#')) left = s[0:i] mid = s[i] right = s[i+1:] if left != '': # Korrektur vlg, leere Textelemente erzeugen Fehler. Bsp. C # (C mit # hochgestellt) self.appendText(left, y, small) if s <> 'bb': self.appendAlteration(y, s[i] == '#', small) s = right if s != '': self.appendText(s, y, small) def createSymbol(self): """ Zusammensetzung eines kompletten Akkordsymbols aus den Komponenten base, high, low als Gruppe von einfachen Textelementen """ self.x = 0 self.groupItems = [] # if self.KS: # self.base = self.base[0].lower() + self.base[1:].lower() # else: # self.base = self.base[0].upper() + self.base[1:].lower() self.appendString(self.base, self.yBase) if self.high != '': self.appendString(self.high, self.yHigh, True) if self.low != '': slash = dict(type='text', x=self.x-0.7*self.relSize, y=self.ySlash, content='/', font=self.slashFont) self.x += 0.1*self.relSize self.groupItems.append(slash) self.appendString(self.low, self.yLow) return dict(type='group', items=self.groupItems) def splitChordSymbolString(self): # self.s aufteilen und Groß-/Kleinschreibung anpassen (high und low sind optional): # +------+ # +------| high | / # | base |------+/ # +------+ /+-----+ # / | low | # / +-----+ self.low = '' if '/' in self.s: # low ist durch / abgetrennt i = self.s.find('/') self.low = self.s[i+1:].strip() self.s = self.s[0:i] self.high = '' # high ist durch Leerzeichen abgetrennt if ' ' in self.s: i = self.s.find(' ') self.high = self.s[i+1:].strip().lower().replace('0', 'o') self.s = self.s[0:i] self.base = self.s.strip() if self.KS: self.base = self.s[0].lower() + self.s[1:].lower() else: self.base = self.s[0].upper() + self.s[1:].lower() if self.low != '': self.low = self.low[0].upper() + self.low[1:].lower() try: if len(self.base) > 1 and self.base[1] in '#b': self.n0 = self.notes.index(self.base[0:2]) else: self.n0 = self.notes.index(self.base[0]) except: self.n0 = -1 try: if len(self.low) > 1 and self.low[1] in '#b': self.n1 = self.notes.index(self.low[0:2]) else: self.n1 = self.notes.index(self.low[0]) except: self.n1 = -1 def create(self): """ Erzeugung des transponierbaren Akkordsymbols """ self.splitChordSymbolString() if self.n0 < 0: return self.createSymbol() else: base1 = self.notes[self.n0] base2 = self.base[len(base1):] transpBase = self.n0 - 8 if self.n1 >= 0: low1 = self.notes[self.n1] low2 = self.low[len(low1):] itemList = [] for i, note in enumerate(self.notes): self.base = note + base2 if self.n1 >= 0: iLow = i + (self.n1-self.n0) iLow = iLow % 21 # korrektur vlg, Falsche Intervallberechnung G/F führt zu F#/E # while iLow >= 21: # iLow -= 12 # while iLow < 0: # iLow += 12 self.low = self.notes[iLow] + low2 itemList.append(self.createSymbol()) return dict(type='transposable', nRefNote=transpBase, items=itemList) def getOptions(major, minor): multipleChoice = len(major) + len(minor) > 1 # Mehrfachauswahl mit Radiobuttons options = ScriptOptions() opt = options.get() size = int(opt.get('size', '10')) sizeStep = size - 6 y = int(opt.get('y', '7')) yStep = y - 6 sansSerif = int(opt.get('sansSerif', '0')) Runter = int(opt.get('Runter', '0')) KleinS = int(opt.get('KleinS', '0')) comboSize = ComboBox([str(i) for i in range(6,15)], value=sizeStep) comboY = ComboBox([str(i) for i in range(6,13)], value=yStep) labelSize = Label('&Größe', width=12) labelY = Label('&Vertikale Lage', width=12) labelChord = Label('&Bezeichnung', width=12) hBox1 = HBox([labelSize, comboSize, Label('Punkt')], padding=8) hBox2 = HBox([labelY, comboY, Label('/2 Zw. über Mittellinie')], padding=8) editSym = Edit (major[0], width=14) if multipleChoice: radioDef = Radio(['&individuell:'], value=0) radioMajor = Radio(major, value=-1, ext=True) radioMinor = Radio(minor, value=-1, ext=True) v1 = VBox([Label('für Dur:'), radioMajor], padding=8) v2 = VBox([Label('für Moll:'), radioMinor], padding=8) h1 = HBox([radioDef, editSym, Label(' ')]) h2 = HBox([Label('Vorschläge: '), v1, v2]) chordBox = VBox([h1, h2], padding=12, text='Bezeichnung') else: chordBox = HBox([labelChord, editSym], padding=8) check = CheckBox ('serifenlose Schrift', value=sansSerif) check1 = CheckBox ('über Notenzeile?', value=Runter) check2 = CheckBox ('Kleinbuchstaben verwenden?', value=KleinS) vBox = VBox([chordBox, hBox1, hBox2, check, check1, check2], padding=16) title = 'Akkordsymbol' dlg = Dialog(title, vBox) #, helpText) result = False if dlg.run(): opt = dict(size=6+comboSize.value(), y=6+comboY.value(), sansSerif=str(check.value()), Runter=str(check1.value()), KleinS=str(check2.value())) if multipleChoice: if radioMajor.value() >= 0: resultString = major[radioMajor.value()] elif radioMinor.value() >= 0: resultString = minor[radioMinor.value()] else: resultString = editSym.value() else: resultString = editSym.value() opt['sym'] = resultString options.set(opt) result = (resultString, 0.5*(6+comboY.value()), 0.1*(6+comboSize.value()), check.value(), check1.value(), check2.value()) return result def getCur(): sel = curSelection() result = (False, False, False, False) if sel == 0: messageBox('Fehler', 'keine aktive Partitur') return result if sel[0] != sel[1]: messageBox('Fehler', 'Markierung ist nicht leer\nDer Cursor muss vor einem Akkord stehen!') return result sel = sel[0] sys = activeScore().system(sel[0]) staff = sys.staff(sel[1]) voice = staff.voice(sel[2]) obj = 0 if sel[3] < voice.nNoteObjs(): obj = voice.noteObj(sel[3]) if obj.isChord(): # and obj.nHeads() > 1: return (sys, staff, voice, obj) messageBox('Fehler', 'Der Cursor steht nicht vor einem Akkord') return result def analyzeChord(pitches): base = pitches[0] chordString = 'CDEFGAB'[base[0]%7] + ('bb','b','','#','+')[base[1]+2] no3 = False I = intervals(pitches) if ((2,-1) in I # kleine Terz and ((4,-2) in I or (3,2) in I) # verm. Quinte oder überm. Quarte and ((6,-2) in I or (5,1) in I)): # verm. Sept. oder gr. Sexte return chordString + ' 0' # "Nullakkord" (wird in kleines o umgewandelt) if (3,0) in I: # reine Quarte chordString += 'sus' elif (2,-1) in I: # kleine Terz: moll chordString += 'm' if (2, 1) not in I and (2,-1) not in I: # keine Terz no3 = True # Intervallziffern: # mod7 | Interv.| verm.| klein| rein | groß |überm.| fehlend #------+--------+------+------+------+------+------+-------- # 1 | None | --- | b9 | --- | 9 | #9 | # 2 | Terz | --- | m | --- |(Std.)| --- | no3 # 3 | Quarte | --- | --- | sus | --- | --- | # 4 | Quinte | b5 | --- |(Std.)| --- | #5 | # 5 | Sexte | --- | 6 | --- | 6 | --- | # 6 | Septime| --- | 7 | --- | maj7 | --- | ggf. "add9" statt 9 # "addb9" statt b9 highString = '' sept = False if (5,1) in I or (5,-1) in I: # kleine und große Sexte bekommen beide "6" highString += '6' if (6,-1) in I: # kleine Septime highString += '7' sept = True elif (6,1) in I: # große Septime highString += 'maj7' sept = True if (4,-2) in I: # verminderte Quinte highString += 'b5' if (1,-1) in I or (1,1) in I: # None (bzw. Sekunde) if not sept: highString += 'add' if (1,-1) in I: # kleine None highString += 'b' highString += '9' if no3: highString += 'no3' if highString != '': chordString += ' ' + highString return [chordString] def analyzeSingleNote(pitch, keyStep): # Schlüssel: Note # Werte : Akkordvorschläge, durch '|' getrennt cMajor = { 'C': 'C|F|Am', 'C#': 'A|C#|G 0', 'Db': 'Eb 7|Fm #5|G 0', 'D': 'G|D|Dm', 'D#': 'B|C 0|E maj7', 'Eb': 'Cm|C 0|Eb', 'E': 'C|A|E', 'E#': 'C#', 'Fb': 'Gb 7', 'F': 'F|G 7|Dm', 'F#': 'D|G maj7|Bm', 'Gb': 'C 0|Eb|Ab 7', 'G': 'G|C|E', 'G#': 'E|F 0|A maj7', 'Ab': 'Fm|Fm #5|B 7', 'A': 'F|A|C 0', 'A#': 'F#', 'Bb': 'C 7|G 0|Gm', 'B': 'G|G 7|C maj7','B#': 'G#'} cMinor = { 'C': 'A|C|F', 'C#': 'A|G 0|D maj7', 'Db': 'Eb 7|E 0|Db', 'D': 'Dm|E 7|F 6', 'D#': 'BE maj7|C 0', 'Eb': 'A 0|Cm|F 7', 'E': 'Am|E|C', 'E#': 'C#', 'Fb': 'Db 0', 'F': 'Dm|G 7|F', 'F#': 'D|G maj7|A 0', 'Gb': 'A 0|Ab 7|Gb', 'G': 'G|Em|C', 'G#': 'E|A maj7|D 0', 'Ab': 'F 0|Bb 7|Ab', 'A': 'Am|Dm|F', 'A#': 'F#', 'Bb': 'Gm|Bb|C 7', 'B': 'E|Em|G', 'B#': 'A 0'} keyNote = RelDiatonicNote.fromCircleOfFifth(keyStep) assert pitch[1] in (-1, 0, 1) # TODO: abfangen relNote = RelDiatonicNote.fromStepAlter(pitch[0]%7, pitch[1]) - keyNote p = str(relNote) choicesMajor = [] choicesMinor = [] def splitNoteExtra(s): if len(s) < 2 or s[1] not in '#b': return (s[0], s[1:]) else: return (s[:2], s[2:]) for a in cMajor[p].split('|'): base, extra = splitNoteExtra(a) baseNote = RelDiatonicNote.fromSymbolic(base) + keyNote choicesMajor.append(str(baseNote) + extra) for a in cMinor[p].split('|'): base, extra = splitNoteExtra(a) baseNote = RelDiatonicNote.fromSymbolic(base) + keyNote choicesMinor.append(str(baseNote) + extra) return (choicesMajor, choicesMinor) def main(): sys, staff, voice, obj = getCur() if obj: time = obj.time() key = obj.curKey() pitches = sys.pitches(time, True) assert len(pitches) > 0 minor = [] if len(pitches) == 1: major, minor = analyzeSingleNote(pitches[0], key) else: major = analyzeChord(pitches) opt = getOptions(major, minor) if opt: (s, y, size, sansSerif, Runter, KleinS) = opt cs = ChordSymbol(obj, s, y, size, sansSerif, Runter, KleinS) sym = cs.create() activeScore().registerUndo("Transponierbares Akkordsymbol") # WICHTIG: registerUndo() ersetzt die Partitur durch ein Duplikat # und speichert das Original zum Rückgängigmachen. # Deshalb muss das Cursorobjekt neu ermittelt werden (geänderte Adresse)! sys, staff, voice, obj = getCur() obj.addDrawObj(sym) main()