Musik, Noten, Midi, Programmierung etc.

Programmieren eines Plugins für Capella in C#

Dies ist eine kurze Anleitung, wie man ein Plug-in für das Notensatzprogramm Capella schreibt in der Programmiersprache C#.

Warum überhaupt?

Capella bietet eine Schnittstelle, mit der man Plugins schreiben kann, die das Programm um weitere Funktionen erweitern. Die Plugins werden üblicherweise mit der Skriptsprache Python 2.5 erstellt. Python 2.5 ist veraltet und wird nicht mehr unterstützt. Dies könnte für Programmierer eine Hürde sein, vor allem wenn sie sonst in C# programmieren.

Für wen?

Diese Anleitung richtet sich an Programmierer, die entsprechende Vorkenntnisse haben. Man sollte zu mindestens in der Lage sein, einfache Konsolenanwendungen oder Winform-Anwendungen mit Visual Studio erstellen zu können. Kenntnisse von LINQ für XLM wären hilfreich.

Ohne ein wenig Python geht es nicht

Ohne ein kleines Python-Skript geht es nicht. Dafür reicht aber eine Vorlage, die man immer nur sehr geringfügig anpassen muss.Im Folgenden wird ein typisches Skript beschrieben.

1	import tempfile
2	import subprocess
3	    if activeScore():
4	    	activeScore().registerUndo('Swingviertel')
5	    	tempFile = tempfile.mktemp('.capx')
6	    	activeScore().write(tempFile)
7	    	return_code = subprocess.call(
                [r"C:\Users\Henning\Documents\SwingViertelCA.exe",tempFile])
8	    	if return_code != 0:
9	       		messageBox('Fehler','Irgendetwas ist schiefgegangen.')
10          	else:
11	     		activeScore().read(tempFile)
12	   	os.remove(tempFile)
    

In Zeile 1 und 2 werden benötigte Bibliotheken importiert. In Zeile 3 wird überprüft, ob eine aktive Partitur vorhanden ist. In Zeile 4 wird der Name das Plugins registriert, damit ein Undo möglich wird. Danach wird eine temporäre Datei erzeugt und die Partitur in dieser temporären Datei abgespeichert. In Zeile 7 wird das externe Programm, das in C# geschrieben wurde, aufgerufen. Das externe Programm nimmt dann die gewünschten Veränderungen an der Partitur vor und speichert sie wieder in der temporären Datei ab. Falls ein Fehler auftritt, wird ein entsprechender Wert ungleich 0 zurückgegeben. Ist dies nicht der Fall, liest Capella Die temporäre Datei ein. Als letztes wird die temporäre Datei gelöscht.

Man kann dieses Skript immer als Vorlage benutzen. Man muss nur den Namen des Plugins und den Pfad zum externen Programm ändern.

Übergabe der Parameter an das C#-Programm

Der Python-Befehl "subprocess.call" erwartet Parameter. Der erste Parameter ist immer das Programm, das aufgerufen werden soll. Der zweite Parameter ist ein Feld mit Zeichenketten (string []). Dieses Feld kann beliebig viele Zeichenketten enthalten, so dass man beliebig viele Daten an das aufgerufene Programm übergeben kann. Es macht Sinn, dass man als erste Zeichenkette den Pfad zu der temporären Datei übergibt

Wenn man z.B. eine Markierung übergeben will, dann kann man durch den Python-Befehl curSelection die Positionen der Cursor erhalten und die einzelnen Zahlen als Zeichenketten übergeben.

Man könnte auch mehrere Funktionen in einem Programm definieren und über die Parameter die auszuführende Funktion übertragen. Dann hätte man viele kleine Python-Dateien, die alle fast gleich aussehen und ein Programm, in dem die ganze Funktionalität gespeichert ist.

Ein kleiner Trick, wie man auch andere Informationen übertragen kann. Man kann Noten farbig machen und die Farbe im externen Programm auswerten.

Den Status der Datei (z.B. klingende oder notierte Ansicht) lässt sich im Datei-Info unterbringen.

Ein einfaches Beispiel

Wenn man Jazz-Stücke vorspielen lässt, kann man mit der Artikulation "swing" dafür sorgen, dass die Achtel ternär vorgespielt werden. Ich möchte jedoch auch, dass die Viertel, wenn sie nicht gebunden sind, staccato gespielt werden.

Dies soll für alle Noten in der Partitur gelten.

Dazu erstelle ich eine Konsolenanwendung in Visual Studio.

Als erstes brauchen wir zwei Hilfsfunktionen, die aus der Capella-Datei die XML-Struktur extrahieren und später die geänderte Struktur in der gleichen Datei wiederspeichert.

Die erste Funktion habe ich GetXMLFromZipFile genannt. Hier ist der Code:

public static XDocument GetXMLFromZipFile(string fn)
        {
            XDocument doc;
            using (var zip = ZipFile.Open(fn, ZipArchiveMode.Read)) {
                var stream = zip.GetEntry("score.xml").Open();
                doc = XDocument.Load(stream);}
            return doc;
        }
    

Eine Capella-Datei kann viele Dateien enthalten. Wir brauchen die Datei mit dem Namen "score.xml". Der Funktion wird der Pfad zur temporären Datei übergeben und die Funktion gibt XDocument-Objekt, das alle Informationen des xml-Dokumentes enthält, zurück. An diesem XDocument werden wir die Änderungen vornehmen. Danach werden wir die Änderungen wieder in der temporären Datei speichern.

Dazu gibt es die Funktion SaveXDocumentToZip:

public static void SaveXDocumentToZip(XDocument doc, string strPath)
    {
        using (var zipToOpen = new FileStream(strPath, FileMode.Open)) {
            using (var archive = new ZipArchive(zipToOpen, ZipArchiveMode.Update)) {
                archive.GetEntry("score.xml").Delete();
                var readmeEntry = archive.CreateEntry("score.xml");
                using (var writer = new StreamWriter(readmeEntry.Open()))
        {  writer.Write(doc.ToString()); } } }
    }
        

Wir löschen die alte Datei in der gezippten Datei und fügen die geänderte Datei zu.

Jetzt zum eigentlichen Programm.

Wir definieren zwei globalen Variablen für die XML-Struktur und den Namensraum.

static XNamespace ns;
static XDocument Xscore;
    

Dann holen wir uns die Daten.

Xscore = GetXMLFromZipFile(args[0]);
ns = Xscore.Root.GetDefaultNamespace();
    

Das Staccato-Zeichen hat im Capella3-Font den Wert 0xC9 oder 201. Mit folgendem Befehl eine entsprechende Zeichenkette erstellt.

string E201 = ((char)201).ToString();
    

Jetzt erzeugen wir den XML-Code für ein Staccato-Element:

var xStaccato = new XElement(ns + "drawObjects", new XElement(ns + "drawObj",
new XElement(ns + "text", new XAttribute("x", "0"), new XAttribute("y", "-4"),
new XElement(ns + "font", new XAttribute("face", "capella3"),
new XAttribute("height", "18"), new XAttribute("charSet", "2"),
new XAttribute("pitchAndFamily", "2"), new XAttribute("color", "FFFFFF")),
new XElement(ns + "content", E201))));
    

Der erzeugte Code sieht folgendermaßen aus:

<drawObjects xmlns="" > 
    <drawObj> 
        <text y="-4" x="0"> 
            <font color="FFFFFF" pitchAndFamily="2" charSet="2" 
            height="18"face="capella3"/> 
            <content>É</content> 
        </text> 
    </drawObj> 
</drawObjects> 
    

Danach muss nur noch die geänderte Datei gespeichert werden.

SaveXDocumentToZip(Xscore, args[0]);
    

Der vollständige Programm-Code sieht so aus:

using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Xml.Linq;
            
namespace SwingViertelCA
{
    class Program
    {
        static XNamespace ns;
        static XDocument Xscore;
        static void Main(string[] args)
        {
            Xscore = GetXMLFromZipFile(args[0]);
            ns = Xscore.Root.GetDefaultNamespace();
            string E201 = ((char)201).ToString();
            var xStaccato = new XElement(ns + "drawObjects", new XElement(ns + "drawObj",
                new XElement(ns + "text", new XAttribute("x", "0"),
                new XAttribute("y", "-4"),new XElement(ns + "font",
                new XAttribute("face", "capella3"), new XAttribute("height", "18"),
                new XAttribute("charSet", "2"),new XAttribute("pitchAndFamily", "2"),
                new XAttribute("color", "FFFFFF")), new XElement(ns + "content", E201))));
            foreach (var chord in Xscore.Descendants(ns + "chord"))
            {
                if (!chord.Descendants(ns + "drawObjects").Any()
                 && !chord.Descendants(ns + "tie").Any())
                {
                    if (chord.Element(ns + "duration").Attribute("dots") == null
                     && chord.Element(ns + "duration").Attribute("base").Value == "1/4")
                    {
                        chord.Element(ns + "duration").AddAfterSelf(xStaccato);
                    }
                }
            }
            SaveXDocumentToZip(Xscore, args[0]);
        }
            
        public static XDocument GetXMLFromZipFile(string fn)
        {
            XDocument doc;
            using (var zip = ZipFile.Open(fn, ZipArchiveMode.Read))
            {
                var stream = zip.GetEntry("score.xml").Open();
                doc = XDocument.Load(stream);
            }
            return doc;
        }
            
        private static void SaveXDocumentToZip(XDocument doc, string strPath)
        {
            using (var zipToOpen = new FileStream(strPath, FileMode.Open)) {
                using (var archive = new ZipArchive(zipToOpen, ZipArchiveMode.Update)) { 
                    archive.GetEntry("score.xml").Delete();
                    var readmeEntry = archive.CreateEntry("score.xml");
                    using (var writer = new StreamWriter(readmeEntry.Open()))
                    {writer.Write(doc.ToString()); } } }
        }
    }
}
    

Dialoge

Will man im Plugin Dialoge, um dem Benutzer Auswahlmöglichen zu gebe, so fügt man der Konsolenanwendeung einfach eine Winform zu und ruft diese im der Main-Funktion auf. Damit man au die Daten des Dialoges zugreifen, müssen die Variablen als public deklariert werden.


© 2025 - Hans-Henning Flessner